You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by fi...@apache.org on 2013/01/22 02:57:58 UTC

[25/52] [partial] support for 2.4.0rc1. "vendored" the platform libs in. added Gord and Braden as contributors. removed dependency on unzip and axed the old download-cordova code.

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java
new file mode 100644
index 0000000..76b0eac
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java
@@ -0,0 +1,502 @@
+/*
+ * 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.capture;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+
+import javax.microedition.media.Manager;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.file.File;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+import org.apache.cordova.util.Logger;
+import org.apache.cordova.util.StringUtils;
+
+/**
+ * This plugin provides the ability to capture media from the native media
+ * applications. The appropriate media application is launched, and a capture
+ * operation is started in the background to identify captured media files and
+ * return the file info back to the caller.
+ */
+public class MediaCapture extends Plugin {
+
+    public static String PROTOCOL_CAPTURE = "capture";
+
+    private static final String LOG_TAG = "MediaCapture: ";
+
+    /**
+     * Error codes.
+     */
+    // Camera or microphone failed to capture image or sound.
+    private static final int CAPTURE_INTERNAL_ERR = 0;
+    // Camera application or audio capture application is currently serving other capture request.
+    private static final int CAPTURE_APPLICATION_BUSY = 1;
+    // Invalid use of the API (e.g. limit parameter has value less than one).
+    private static final int CAPTURE_INVALID_ARGUMENT = 2;
+    // User exited camera application or audio capture application before capturing anything.
+    private static final int CAPTURE_NO_MEDIA_FILES = 3;
+    // The requested capture operation is not supported.
+    private static final int CAPTURE_NOT_SUPPORTED = 20;
+
+    /**
+     * Possible actions.
+     */
+    protected static final String ACTION_GET_SUPPORTED_MODES = "captureModes";
+    protected static final String ACTION_CAPTURE_AUDIO = "captureAudio";
+    protected static final String ACTION_CAPTURE_IMAGE = "captureImage";
+    protected static final String ACTION_CAPTURE_VIDEO = "captureVideo";
+    protected static final String ACTION_CANCEL_CAPTURES = "stopCaptures";
+
+    /**
+     * Executes the requested action and returns a PluginResult.
+     *
+     * @param action
+     *            The action to execute.
+     * @param callbackId
+     *            The callback ID to be invoked upon action completion
+     * @param args
+     *            JSONArry of arguments for the action.
+     * @return A PluginResult object with a status and message.
+     */
+    public PluginResult execute(String action, JSONArray args, String callbackId) {
+        PluginResult result = null;
+
+        if (ACTION_GET_SUPPORTED_MODES.equals(action)) {
+            result = getCaptureModes();
+        } else if (ACTION_CAPTURE_AUDIO.equals(action)) {
+            result = captureAudio(args, callbackId);
+        } else if (ACTION_CAPTURE_IMAGE.equals(action)) {
+            result = captureImage(args, callbackId);
+        } else if (ACTION_CAPTURE_VIDEO.equals(action)) {
+            result = captureVideo(args, callbackId);
+        } else if (ACTION_CANCEL_CAPTURES.equals(action)) {
+            CaptureControl.getCaptureControl().stopPendingOperations(true);
+            result =  new PluginResult(PluginResult.Status.OK);
+        } else {
+            result = new PluginResult(PluginResult.Status.INVALID_ACTION,
+                "MediaCapture: invalid action " + action);
+        }
+
+        return result;
+    }
+
+    /**
+     * Determines if audio capture is supported.
+     * @return <code>true</code> if audio capture is supported
+     */
+    protected boolean isAudioCaptureSupported() {
+        return (System.getProperty("supports.audio.capture").equals(Boolean.TRUE.toString())
+                && AudioControl.hasAudioRecorderApplication());
+    }
+
+    /**
+     * Determines if video capture is supported.
+     * @return <code>true</code> if video capture is supported
+     */
+    protected boolean isVideoCaptureSupported() {
+        return (System.getProperty("supports.video.capture").equals(Boolean.TRUE.toString()));
+    }
+
+    /**
+     * Return the supported capture modes for audio, image and video.
+     * @return supported capture modes.
+     */
+    private PluginResult getCaptureModes() {
+        JSONArray audioModes = new JSONArray();
+        JSONArray imageModes = new JSONArray();
+        boolean audioSupported = isAudioCaptureSupported();
+
+        // need to get the recording dimensions from supported image encodings
+        String imageEncodings = System.getProperty("video.snapshot.encodings");
+        Logger.log(this.getClass().getName() + ": video.snapshot.encodings="
+                + imageEncodings);
+        String[] encodings = StringUtils.split(imageEncodings, "encoding=");
+        CaptureMode mode = null;
+        Vector list = new Vector();
+
+        // get all supported capture content types for audio and image
+        String[] contentTypes = getCaptureContentTypes();
+        for (int i = 0; i < contentTypes.length; i++) {
+            if (audioSupported
+                    && contentTypes[i]
+                            .startsWith(AudioCaptureOperation.CONTENT_TYPE)) {
+                audioModes.add(new CaptureMode(contentTypes[i]).toJSONObject());
+            } else if (contentTypes[i]
+                    .startsWith(ImageCaptureOperation.CONTENT_TYPE)) {
+                String type = contentTypes[i]
+                        .substring(ImageCaptureOperation.CONTENT_TYPE.length());
+                for (int j = 0; j < encodings.length; j++) {
+                    // format: "jpeg&width=2592&height=1944 "
+                    String enc = encodings[j];
+                    if (enc.startsWith(type)) {
+                        Hashtable parms = parseEncodingString(enc);
+                        // "width="
+                        String w = (String)parms.get("width");
+                        long width = (w == null) ? 0 : Long.parseLong(w);
+                        // "height="
+                        String h = (String)parms.get("height");
+                        long height = (h == null) ? 0 : Long.parseLong(h);
+                        // new capture mode
+                        mode = new CaptureMode(contentTypes[i], width, height);
+                        // don't want duplicates
+                        if (!list.contains(mode)) {
+                            list.addElement(mode);
+                            imageModes.add(mode.toJSONObject());
+                        }
+                    }
+                }
+            }
+        }
+
+        JSONObject captureModes = new JSONObject();
+        try {
+            captureModes.put("supportedAudioModes", audioModes.toString());
+            captureModes.put("supportedImageModes", imageModes.toString());
+            captureModes.put("supportedVideoModes", getVideoCaptureModes().toString());
+        } catch (JSONException e) {
+            Logger.error("JSONException: " + e.getMessage());
+            return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                    "Failed to build supported capture modes.");
+        }
+
+        return new PluginResult(PluginResult.Status.OK, captureModes);
+    }
+
+    /**
+     * Retrieves supported video capture modes (content type, width and height).
+     * @return supported video capture modes
+     */
+    protected JSONArray getVideoCaptureModes() {
+        JSONArray videoModes = new JSONArray();
+
+        if (!isVideoCaptureSupported()) {
+            // if the device does not support video capture, return an empty
+            // array of capture modes
+            Logger.log(this.getClass().getName() + ": video capture not supported");
+            return videoModes;
+        }
+
+        /**
+         * DOH! Even if video capture is supported, BlackBerry's API
+         * does not provide any 'video/' content types for the 'capture'
+         * protocol.  So if we looked at only capture content types,
+         * it wouldn't return any results...
+         *
+         * // get all supported capture content types
+         * String[] contentTypes = getCaptureContentTypes();
+         *
+         * A better alternative, and probably not too inaccurate, would be to
+         * send back all supported video modes (not just capture).  This will
+         * at least give the developer an idea of the capabilities.
+         */
+
+        // retrieve ALL supported video encodings
+        String videoEncodings = System.getProperty("video.encodings");
+        Logger.log(this.getClass().getName() + ": video.encodings=" + videoEncodings);
+        String[] encodings = StringUtils.split(videoEncodings, "encoding=");
+
+        // parse them into CaptureModes
+        String enc = null;
+        CaptureMode mode = null;
+        Vector list = new Vector();
+        for (int i = 0; i < encodings.length; i++) {
+            enc = encodings[i];
+            // format: "video/3gpp&width=640&height=480&video_codec=MPEG-4&audio_codec=AAC "
+            if (enc.startsWith(VideoCaptureOperation.CONTENT_TYPE)) {
+                Hashtable parms = parseEncodingString(enc);
+                // type "video/3gpp"
+                String t = (String)parms.get("type");
+                // "width="
+                String w = (String)parms.get("width");
+                long width = (w == null) ? 0 : Long.parseLong(w);
+                // "height="
+                String h = (String)parms.get("height");
+                long height = (h == null) ? 0 : Long.parseLong(h);
+                // new capture mode
+                mode = new CaptureMode(t, width, height);
+                // don't want duplicates
+                if (!list.contains(mode)) {
+                    list.addElement(mode);
+                    videoModes.add(mode.toJSONObject());
+                }
+            }
+        }
+
+        return videoModes;
+    }
+
+    /**
+     * Utility method to parse encoding strings.
+     *
+     * @param encodingString
+     *            encoding string
+     * @return Hashtable containing key:value pairs
+     */
+    protected Hashtable parseEncodingString(final String encodingString) {
+        // format: "video/3gpp&width=640&height=480&video_codec=MPEG-4&audio_codec=AAC "
+        Hashtable props = new Hashtable();
+        String[] parms = StringUtils.split(encodingString, "&");
+        props.put("type", parms[0]);
+        for (int i = 0; i < parms.length; i++) {
+            String parameter = parms[i];
+            if (parameter.indexOf('=') != -1) {
+                String[] pair = StringUtils.split(parameter, "=");
+                props.put(pair[0].trim(), pair[1].trim());
+            }
+        }
+        return props;
+    }
+
+    /**
+     * Returns the content types supported for the <code>capture://</code>
+     * protocol.
+     *
+     * @return list of supported capture content types
+     */
+    protected static String[] getCaptureContentTypes() {
+        // retrieve list of all content types supported for capture protocol
+        return Manager.getSupportedContentTypes(PROTOCOL_CAPTURE);
+    }
+
+    /**
+     * Starts an audio capture operation using the native voice notes recorder
+     * application. If the native voice notes recorder application is already
+     * running, the <code>CAPTURE_APPLICATION_BUSY</code> error is returned.
+     *
+     * @param args
+     *            capture options (e.g., limit)
+     * @param callbackId
+     *            the callback to be invoked with the capture results
+     * @return PluginResult containing captured media file properties
+     */
+    protected PluginResult captureAudio(final JSONArray args, final String callbackId) {
+        PluginResult result = null;
+
+        // if audio is not being recorded, start audio capture
+        if (!AudioControl.hasAudioRecorderApplication()) {
+            result = errorResult(CAPTURE_NOT_SUPPORTED,
+                    "Audio recorder application is not installed.");
+        } else if (AudioControl.isAudioRecorderActive()) {
+            result = errorResult(CAPTURE_APPLICATION_BUSY,
+                    "Audio recorder application is busy.");
+        }
+        else {
+            // optional parameters
+            long limit = 1;
+            double duration = 0.0f;
+
+            try {
+                JSONObject options = args.getJSONObject(0);
+                if (options != null) {
+                    limit = options.optLong("limit", 1);
+                    duration = options.optDouble("duration", 0.0f);
+                }
+            } catch (JSONException e) {
+                // Eat it and use default value of 1.
+                Logger.log(this.getClass().getName()
+                        + ": Invalid captureAudio options format. " + e.getMessage());
+            }
+
+            // start audio capture
+            // start capture operation in the background
+            CaptureControl.getCaptureControl().startAudioCaptureOperation(
+                    limit, duration, callbackId);
+
+            // return NO_RESULT and allow callbacks to be invoked later
+            result = new PluginResult(PluginResult.Status.NO_RESULT);
+            result.setKeepCallback(true);
+        }
+
+        return result;
+    }
+
+    /**
+     * Starts an image capture operation using the native camera application. If
+     * the native camera application is already running, the
+     * <code>CAPTURE_APPLICATION_BUSY</code> error is returned.
+     *
+     * @param args
+     *            capture options (e.g., limit)
+     * @param callbackId
+     *            the callback to be invoked with the capture results
+     * @return PluginResult containing captured media file properties
+     */
+    protected PluginResult captureImage(final JSONArray args,
+            final String callbackId) {
+        PluginResult result = null;
+
+        if (CameraControl.isCameraActive()) {
+            result = errorResult(CAPTURE_APPLICATION_BUSY,
+                    "Camera application is busy.");
+        }
+        else {
+            // optional parameters
+            long limit = 1;
+
+            try {
+                JSONObject options = args.getJSONObject(0);
+                if (options != null) {
+                    limit = options.optLong("limit", 1);
+                }
+            } catch (JSONException e) {
+                // Eat it and use default value of 1.
+                Logger.log(this.getClass().getName()
+                        + ": Invalid captureImage options format. " + e.getMessage());
+            }
+
+            // start capture operation in the background
+            CaptureControl.getCaptureControl().startImageCaptureOperation(
+                    limit, callbackId);
+
+            // return NO_RESULT and allow callbacks to be invoked later
+            result = new PluginResult(PluginResult.Status.NO_RESULT);
+            result.setKeepCallback(true);
+        }
+
+        return result;
+    }
+
+    /**
+     * Starts an video capture operation using the native video recorder
+     * application. If the native video recorder application is already running,
+     * the <code>CAPTURE_APPLICATION_BUSY</code> error is returned.
+     *
+     * @param args
+     *            capture options (e.g., limit)
+     * @param callbackId
+     *            the callback to be invoked with the capture results
+     * @return PluginResult containing captured media file properties
+     */
+    protected PluginResult captureVideo(final JSONArray args,
+            final String callbackId) {
+        PluginResult result = null;
+
+        if (!isVideoCaptureSupported()) {
+            result = errorResult(CAPTURE_NOT_SUPPORTED,
+                    "Video capture is not supported.");
+        } else if (CameraControl.isVideoRecorderActive()) {
+            result = errorResult(CAPTURE_APPLICATION_BUSY,
+                    "Video recorder application is busy.");
+        }
+        else {
+            // optional parameters
+            long limit = 1;
+
+            try {
+                JSONObject options = args.getJSONObject(0);
+                if (options != null) {
+                    limit = options.optLong("limit", 1);
+                }
+            } catch (JSONException e) {
+                // Eat it and use default value of 1.
+                Logger.log(this.getClass().getName()
+                        + ": Invalid captureVideo options format. " + e.getMessage());
+            }
+
+            // start capture operation in the background
+            CaptureControl.getCaptureControl().startVideoCaptureOperation(
+                    limit, callbackId);
+
+            // return NO_RESULT and allow callbacks to be invoked later
+            result = new PluginResult(PluginResult.Status.NO_RESULT);
+            result.setKeepCallback(true);
+        }
+
+        return result;
+    }
+
+    /**
+     * Sends media capture result back to JavaScript.
+     *
+     * @param mediaFiles
+     *            list of File objects describing captured media files
+     * @param callbackId
+     *            the callback to receive the file descriptions
+     */
+    public static void captureSuccess(Vector mediaFiles, String callbackId) {
+        PluginResult result = null;
+        File file = null;
+
+        JSONArray array = new JSONArray();
+        for (Enumeration e = mediaFiles.elements(); e.hasMoreElements();) {
+            file = (File) e.nextElement();
+            array.add(file.toJSONObject());
+        }
+
+        // invoke the appropriate callback
+        result = new PluginResult(PluginResult.Status.OK, array);
+        success(result, callbackId);
+    }
+
+    /**
+     * Sends error back to JavaScript.
+     *
+     * @param callbackId
+     *            the callback to receive the error
+     */
+    public static void captureError(String callbackId) {
+        error(errorResult(CAPTURE_NO_MEDIA_FILES, ""), callbackId);
+    }
+
+    /**
+     * Called when application is resumed.
+     */
+    public void onResume() {
+        // We launch the native media applications for capture operations, which
+        // puts this application in the background.  This application will come
+        // to the foreground when the user closes the native media application.
+        // So we close any running capture operations any time we resume.
+        //
+        // It would be nice if we could catch the EVT_APP_FOREGROUND event that
+        // is supposed to be triggered when the application comes to the
+        // foreground, but have not seen a way to do that on the Java side.
+        // Unfortunately, we have to get notification from the JavaScript side,
+        // which does get the event.  (Argh! Only BlackBerry.)
+        //
+        // In this case, we're just stopping the capture operations, not
+        // canceling them.
+        CaptureControl.getCaptureControl().stopPendingOperations(false);
+    }
+
+    /**
+     * Invoked when this application terminates.
+     */
+    public void onDestroy() {
+        CaptureControl.getCaptureControl().stopPendingOperations(true);
+    }
+
+    private static PluginResult errorResult(int code, String message) {
+        Logger.log(LOG_TAG + message);
+
+        JSONObject obj = new JSONObject();
+        try {
+            obj.put("code", code);
+            obj.put("message", message);
+        } catch (JSONException e) {
+            // This will never happen
+        }
+
+        return new PluginResult(PluginResult.Status.ERROR, obj);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
new file mode 100644
index 0000000..56ffff5
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cordova.capture;
+
+import java.util.Vector;
+
+/**
+ * Acts as a container for captured media files.  The media applications will
+ * add to the queue when a media file is captured.
+ */
+class MediaQueue {
+    private Vector queue = new Vector();
+
+    synchronized void add(final String filePath) {
+        queue.addElement(filePath);
+        notifyAll();
+    }
+
+    synchronized String remove() throws InterruptedException {
+        while (queue.size() == 0) {
+            wait();
+        }
+        String filePath = (String) queue.firstElement();
+        queue.removeElement(filePath);
+        notifyAll();
+        return filePath;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java
new file mode 100644
index 0000000..361b7ee
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java
@@ -0,0 +1,107 @@
+/*
+ * 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.capture;
+
+import net.rim.device.api.io.file.FileSystemJournal;
+import net.rim.device.api.io.file.FileSystemJournalEntry;
+import net.rim.device.api.io.file.FileSystemJournalListener;
+
+/**
+ * Listens for video recording files that are added to file system.
+ * <p>
+ * Video recordings are added to the file system in a multi-step process. The
+ * video recorder application records the video on a background thread. While
+ * the recording is in progress, it is added to the file system with a '.lock'
+ * extension. When the user stops the recording, the file is renamed to the
+ * video recorder extension (e.g. .3GP). Therefore, we listen for the
+ * <code>FileSystemJournalEntry.FILE_RENAMED</code> event, capturing when the
+ * new path name ends in the video recording file extension.
+ * <p>
+ * The file system notifications will arrive on the application event thread.
+ * When it receives a notification, it adds the image file path to a MediaQueue
+ * so that the capture thread can process the file.
+ */
+class VideoCaptureListener implements FileSystemJournalListener {
+
+    /**
+     * Used to track file system changes.
+     */
+    private long lastUSN = 0;
+
+    /**
+     * Queue to send media files to for processing.
+     */
+    private MediaQueue queue = null;
+
+    /**
+     * Newly added video recording.
+     */
+    private String newFilePath = null;
+
+    /**
+     * Constructor.
+     */
+    VideoCaptureListener(MediaQueue queue) {
+        this.queue = queue;
+    }
+
+    public void fileJournalChanged() {
+        // next sequence number file system will use
+        long USN = FileSystemJournal.getNextUSN();
+
+        for (long i = USN - 1; i >= lastUSN && i < USN; --i)
+        {
+            FileSystemJournalEntry entry = FileSystemJournal.getEntry(i);
+            if (entry == null)
+            {
+                break;
+            }
+
+            String path = entry.getPath();
+            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED
+                    && newFilePath == null) {
+                // a new file has been added to the file system
+                // if it has a video recording extension, store it until
+                // it is renamed, indicating it has finished being written to
+                int index = path.indexOf(".3GP");
+                if (index == -1) {
+                    index = path.indexOf(".MP4");
+                }
+                if (index != -1) {
+                    newFilePath = path.substring(0, index + 4);
+                }
+            }
+            else if (entry.getEvent() == FileSystemJournalEntry.FILE_RENAMED) {
+                if (path != null && path.equals(newFilePath))
+                {
+                    // add file path to the capture queue
+                    queue.add("file://" + path);
+
+                    // get ready for next file
+                    newFilePath = null;
+                    break;
+                }
+            }
+        }
+
+        // remember the file journal change number,
+        // so we don't search the same events again and again
+        lastUSN = USN;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java
new file mode 100644
index 0000000..589bc68
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java
@@ -0,0 +1,124 @@
+/*
+ * 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.capture;
+
+import java.io.IOException;
+
+import javax.microedition.io.Connector;
+import javax.microedition.io.file.FileConnection;
+
+import org.apache.cordova.file.File;
+import org.apache.cordova.util.FileUtils;
+import org.apache.cordova.util.Logger;
+
+import net.rim.device.api.io.MIMETypeAssociations;
+import net.rim.device.api.ui.UiApplication;
+
+public class VideoCaptureOperation extends CaptureOperation {
+
+    // content type
+    public static String CONTENT_TYPE = "video/";
+
+    // file system listener
+    private VideoCaptureListener listener = null;
+
+    /**
+     * Creates and starts an image capture operation.
+     *
+     * @param limit
+     *            maximum number of media files to capture
+     * @param callbackId
+     *            the callback to receive the files
+     * @param queue
+     *            the queue from which to retrieve captured media files
+     */
+    public VideoCaptureOperation(long limit, String callbackId, MediaQueue queue) {
+        super(limit, callbackId, queue);
+
+        // listener to capture image files added to file system
+        this.listener = new VideoCaptureListener(queue);
+
+        start();
+    }
+
+    /**
+     * Registers file system listener and launches native video recorder
+     * application.
+     */
+    protected void setup() {
+        // register listener for files being written
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().addFileSystemJournalListener(listener);
+        }
+
+        // launch the native video recorder application
+        CameraControl.launchVideoRecorder();
+    }
+
+    /**
+     * Unregisters file system listener and closes native video recorder
+     * application.
+     */
+    protected void teardown() {
+        // remove file system listener
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().removeFileSystemJournalListener(listener);
+        }
+
+        // close the native video recorder application
+        CameraControl.closeVideoRecorder();
+    }
+
+    /**
+     * Retrieves the file properties for the captured video recording.
+     *
+     * @param filePath
+     *            full path of the video recording file
+     */
+    protected void processFile(String filePath) {
+        Logger.log(this.getClass().getName() + ": processing file: " + filePath);
+
+        File file = new File(FileUtils.stripSeparator(filePath));
+
+        // grab file properties
+        FileConnection fconn = null;
+        try {
+            fconn = (FileConnection) Connector.open(filePath, Connector.READ);
+            if (fconn.exists()) {
+                long size = fconn.fileSize();
+                Logger.log(this.getClass().getName() + ": " + filePath + " size="
+                        + Long.toString(size) + " bytes");
+                file.setLastModifiedDate(fconn.lastModified());
+                file.setName(FileUtils.stripSeparator(fconn.getName()));
+                file.setSize(size);
+                file.setType(MIMETypeAssociations.getMIMEType(filePath));
+            }
+        }
+        catch (IOException e) {
+            Logger.log(this.getClass().getName() + ": " + e);
+        }
+        finally {
+            try {
+                if (fconn != null) fconn.close();
+            } catch (IOException ignored) {}
+        }
+
+        addCaptureFile(file);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
new file mode 100644
index 0000000..e11f924
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Copyright (c) 2011, Research In Motion Limited.
+ */
+package org.apache.cordova.device;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+import net.rim.device.api.system.DeviceInfo;
+
+/**
+ * Provides device information, including:
+ *
+ * - Device platform version (e.g. 2.13.0.95). Not to be confused with BlackBerry OS version.
+ * - Unique device identifier (UUID).
+ * - Cordova software version.
+ */
+public final class Device extends Plugin {
+
+	public static final String FIELD_PLATFORM 	= "platform";
+	public static final String FIELD_UUID     	= "uuid";
+	public static final String FIELD_CORDOVA	= "cordova";
+	public static final String FIELD_MODEL 		= "model";
+	public static final String FIELD_NAME 		= "name";
+	public static final String FIELD_VERSION 	= "version";
+
+	public static final String ACTION_GET_DEVICE_INFO = "getDeviceInfo";
+
+	public PluginResult execute(String action, JSONArray args, String callbackId) {
+		PluginResult result = new PluginResult(PluginResult.Status.INVALID_ACTION, "Device: Invalid action:" + action);
+
+		if(action.equals(ACTION_GET_DEVICE_INFO)){
+			try {
+				JSONObject device = new JSONObject();
+				device.put( FIELD_PLATFORM, "BlackBerry");
+				device.put( FIELD_UUID, new Integer( DeviceInfo.getDeviceId()) );
+				device.put( FIELD_CORDOVA, "2.4.0rc1" );
+				device.put( FIELD_MODEL, new String(DeviceInfo.getDeviceName()) );
+				device.put( FIELD_NAME, new String(DeviceInfo.getDeviceName()) );
+				device.put( FIELD_VERSION, new String(DeviceInfo.getSoftwareVersion()) );
+				result = new PluginResult(PluginResult.Status.OK, device);
+			} catch (JSONException e) {
+				result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
+			}
+		}
+
+		return result;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
new file mode 100644
index 0000000..66fb59b
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cordova.file;
+
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+public class Entry {
+
+    private boolean isDirectory = false;
+    private String name = null;
+    private String fullPath = null;
+
+    public boolean isDirectory() {
+        return isDirectory;
+    }
+
+    public void setDirectory(boolean isDirectory) {
+        this.isDirectory = isDirectory;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getFullPath() {
+        return fullPath;
+    }
+
+    public void setFullPath(String fullPath) {
+        this.fullPath = fullPath;
+    }
+
+    public JSONObject toJSONObject() {
+        JSONObject o = new JSONObject();
+        try {
+            o.put("isDirectory", isDirectory);
+            o.put("isFile", !isDirectory);
+            o.put("name", name);
+            o.put("fullPath", fullPath);
+        }
+        catch (JSONException ignored) {
+        }
+        return o;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java
new file mode 100644
index 0000000..3d04041
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java
@@ -0,0 +1,84 @@
+/*
+ * 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 org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+public class File {
+    private String name = null;
+    private String fullPath = null;
+    private String type = null;
+    private long lastModifiedDate;
+    private long size = 0;
+
+    public File(String filePath) {
+        this.fullPath = filePath;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public long getLastModifiedDate() {
+        return lastModifiedDate;
+    }
+
+    public void setLastModifiedDate(long lastModifiedDate) {
+        this.lastModifiedDate = lastModifiedDate;
+    }
+
+    public long getSize() {
+        return size;
+    }
+
+    public void setSize(long size) {
+        this.size = size;
+    }
+
+    public String getFullPath() {
+        return fullPath;
+    }
+
+    public JSONObject toJSONObject() {
+        JSONObject o = new JSONObject();
+        try {
+            o.put("fullPath", fullPath);
+            o.put("type", type);
+            o.put("name", name);
+            o.put("lastModifiedDate", lastModifiedDate);
+            o.put("size", size);
+        }
+        catch (JSONException ignored) {
+        }
+        return o;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
new file mode 100644
index 0000000..e0c3556
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
@@ -0,0 +1,1044 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Enumeration;
+
+import javax.microedition.io.Connector;
+import javax.microedition.io.file.FileConnection;
+import javax.microedition.io.file.FileSystemRegistry;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+import org.apache.cordova.util.FileUtils;
+import org.apache.cordova.util.Logger;
+
+import net.rim.device.api.io.Base64OutputStream;
+import net.rim.device.api.io.FileNotFoundException;
+import net.rim.device.api.io.MIMETypeAssociations;
+import net.rim.device.api.system.Application;
+
+public class FileManager extends Plugin {
+
+    /**
+     * File related errors.
+     */
+    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;
+
+    /**
+     * File system for storing information on a temporary basis (no guaranteed persistence).
+     */
+    public static final short FS_TEMPORARY = 0;
+
+    /**
+     * File system for storing information on a permanent basis.
+     */
+    public static final short FS_PERSISTENT = 1;
+
+    /**
+     * Possible actions.
+     */
+    protected static String ACTION_READ_AS_TEXT = "readAsText";
+    protected static String ACTION_READ_AS_DATA_URL = "readAsDataURL";
+    protected static String ACTION_WRITE = "write";
+    protected static String ACTION_TRUNCATE = "truncate";
+    protected static String ACTION_REQUEST_FILE_SYSTEM = "requestFileSystem";
+    protected static String ACTION_RESOLVE_FILE_SYSTEM_URI = "resolveLocalFileSystemURI";
+    protected static String ACTION_GET_METADATA = "getMetadata";
+    protected static String ACTION_GET_FILE_METADATA = "getFileMetadata";
+    protected static String ACTION_LIST_DIRECTORY = "readEntries";
+    protected static String ACTION_COPY_TO = "copyTo";
+    protected static String ACTION_MOVE_TO = "moveTo";
+    protected static String ACTION_IS_FILE_SYSTEM_ROOT = "isFileSystemRoot";
+
+    /**
+     * Executes the requested action and returns a PluginResult.
+     *
+     * @param action
+     *            The action to execute.
+     * @param callbackId
+     *            The callback ID to be invoked upon action completion
+     * @param args
+     *            JSONArry of arguments for the action.
+     * @return A PluginResult object with a status and message.
+     */
+    public PluginResult execute(String action, JSONArray args, String callbackId) {
+
+        // perform specified action
+        if (ACTION_READ_AS_TEXT.equals(action)) {
+            // get file path
+            String filePath = null;
+            try {
+                filePath = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return readAsText(filePath, args.optString(1));
+        }
+        else if (ACTION_READ_AS_DATA_URL.equals(action)) {
+            // get file path
+            String filePath = null;
+            try {
+                filePath = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return readAsDataURL(filePath);
+        }
+        else if (ACTION_WRITE.equals(action)) {
+            // file path
+            String filePath = null;
+            try {
+                filePath = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+
+            // file data
+            String data = null;
+            try {
+                data = args.getString(1);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Unable to parse file data: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+
+            // position
+            int position = 0;
+            try {
+                position = Integer.parseInt(args.optString(2));
+            }
+            catch (NumberFormatException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid position parameter: " + e);
+                return new PluginResult(
+                        PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return writeFile(filePath, data, position);
+        }
+        else if (ACTION_TRUNCATE.equals(action)) {
+            // file path
+            String filePath = null;
+            try {
+                filePath = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+
+            // file size
+            long fileSize = 0;
+            try {
+                // retrieve new file size
+                fileSize = Long.parseLong(args.getString(1));
+            }
+            catch (Exception e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid file size parameter: " + e);
+                return new PluginResult(
+                        PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return truncateFile(filePath, fileSize);
+        }
+        else if (ACTION_REQUEST_FILE_SYSTEM.equals(action)) {
+            int fileSystemType = -1;
+            long fileSystemSize = 0;
+            try {
+                fileSystemType = args.getInt(0);
+                fileSystemSize = (args.isNull(1) == true) ? 0 : args.getLong(1);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid file system type: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return requestFileSystem(fileSystemType, fileSystemSize);
+        }
+        else if (ACTION_RESOLVE_FILE_SYSTEM_URI.equals(action)) {
+            String uri = null;
+            try {
+                uri = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing file URI: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return resolveFileSystemURI(uri);
+        }
+        else if (ACTION_GET_METADATA.equals(action) || ACTION_GET_FILE_METADATA.equals(action)) {
+            String path = null;
+            try {
+                path = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing file URI: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return getMetadata(path, ACTION_GET_FILE_METADATA.equals(action));
+        }
+        else if (ACTION_LIST_DIRECTORY.equals(action)) {
+            String path = null;
+            try {
+                path = args.getString(0);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return listDirectory(path);
+        }
+        else if (ACTION_COPY_TO.equals(action)) {
+            String srcPath = null;
+            String parent = null;
+            String newName = null;
+            try {
+                srcPath = args.getString(0);
+                parent = args.getString(1);
+                newName = args.getString(2);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return copyTo(srcPath, parent, newName);
+        }
+        else if (ACTION_MOVE_TO.equals(action)) {
+            String srcPath = null;
+            String parent = null;
+            String newName = null;
+            try {
+                srcPath = args.getString(0);
+                parent = args.getString(1);
+                newName = args.getString(2);
+            }
+            catch (JSONException e) {
+                Logger.log(this.getClass().getName()
+                        + ": Invalid or missing path: " + e);
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                        SYNTAX_ERR);
+            }
+            return moveTo(srcPath, parent, newName);
+        }
+        else if (ACTION_IS_FILE_SYSTEM_ROOT.equals(action)) {
+            return new PluginResult(PluginResult.Status.OK,
+                    isFileSystemRoot(args.optString(0)));
+        }
+
+        // invalid action
+        return new PluginResult(PluginResult.Status.INVALID_ACTION,
+                "File: invalid action " + action);
+    }
+
+    /**
+     * Reads a file and encodes the contents using the specified encoding.
+     *
+     * @param filePath
+     *            Full path of the file to be read
+     * @param encoding
+     *            Encoding to use for the file contents
+     * @return PluginResult containing encoded file contents or error code if
+     *         unable to read or encode file
+     */
+    protected static PluginResult readAsText(String filePath, String encoding) {
+        PluginResult result = null;
+        String logMsg = ": encoding file contents using " + encoding;
+
+        // read the file
+        try {
+            // return encoded file contents
+            byte[] blob = FileUtils.readFile(filePath, Connector.READ);
+            result = new PluginResult(PluginResult.Status.OK,
+                    new String(blob, encoding));
+        }
+        catch (FileNotFoundException e) {
+            logMsg = e.toString();
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+        catch (UnsupportedEncodingException e) {
+            logMsg = e.toString();
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    ENCODING_ERR);
+        }
+        catch (IOException e) {
+            logMsg = e.toString();
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_READABLE_ERR);
+        }
+        finally {
+            Logger.log(FileManager.class.getName() + ": " + logMsg);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read file and return data as a base64 encoded data url. A data url is of
+     * the form: data:[<mediatype>][;base64],<data>
+     *
+     * @param filePath
+     *            Full path of the file to be read
+     * @return PluginResult containing the encoded file contents or an error
+     *         code if unable to read the file
+     */
+    protected static PluginResult readAsDataURL(String filePath) {
+        String data = null;
+        try {
+            // read file
+            byte[] blob = FileUtils.readFile(filePath, Connector.READ);
+
+            // encode file contents using BASE64 encoding
+            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            Base64OutputStream base64OutputStream = new Base64OutputStream(
+                    byteArrayOutputStream);
+            base64OutputStream.write(blob);
+            base64OutputStream.flush();
+            base64OutputStream.close();
+            data = byteArrayOutputStream.toString();
+        }
+        catch (FileNotFoundException e) {
+            Logger.log(FileManager.class.getName() + ": " + e);
+            return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+        catch (IOException e) {
+            Logger.log(FileManager.class.getName() + ": " + e);
+            return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_READABLE_ERR);
+        }
+
+        // put result in proper form
+        String mediaType = MIMETypeAssociations.getMIMEType(filePath);
+        if (mediaType == null) {
+            mediaType = "";
+        }
+        data = "data:" + mediaType + ";base64," + data;
+
+        return new PluginResult(PluginResult.Status.OK, data);
+    }
+
+    /**
+     * Writes data to the specified file.
+     *
+     * @param filePath
+     *            Full path of file to be written to
+     * @param data
+     *            Data to be written
+     * @param position
+     *            Position at which to begin writing
+     * @return PluginResult containing the number of bytes written or error code
+     *         if unable to write file
+     */
+    protected static PluginResult writeFile(String filePath, String data, int position) {
+        PluginResult result = null;
+        int bytesWritten = 0;
+        try {
+            // write file data
+            // The default String encoding on BB is ISO-8859-1 which causes
+            // issues with extended characters.  Force to UTF-8 to provide
+            // greater character support and match other platforms.
+            bytesWritten = FileUtils.writeFile(filePath, data.getBytes("UTF-8"), position);
+            result = new PluginResult(PluginResult.Status.OK, bytesWritten);
+        }
+        catch (SecurityException e) {
+            Logger.log(FileManager.class.getName() + ": " + e);
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NO_MODIFICATION_ALLOWED_ERR);
+        }
+        catch (IOException e) {
+            // it's not a security issue, so the directory path is either
+            // not fully created or a general error occurred
+            Logger.log(FileManager.class.getName() + ": " + e);
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+
+        return result;
+    }
+
+    /**
+     * Changes the length of the specified file. If shortening, data beyond new
+     * length is discarded.
+     *
+     * @param fileName
+     *            The full path of the file to truncate
+     * @param size
+     *            The size to which the length of the file is to be adjusted
+     * @return PluginResult containing new file size or an error code if an
+     *         error occurred
+     */
+    protected static PluginResult truncateFile(String filePath, long size) {
+        long fileSize = 0;
+        FileConnection fconn = null;
+        try {
+            fconn = (FileConnection) Connector.open(filePath,
+                    Connector.READ_WRITE);
+            if (!fconn.exists()) {
+                Logger.log(FileManager.class.getName() + ": path not found "
+                        + filePath);
+                return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                        NOT_FOUND_ERR);
+            }
+            if (size >= 0) {
+                fconn.truncate(size);
+            }
+            fileSize = fconn.fileSize();
+        }
+        catch (IOException e) {
+            Logger.log(FileManager.class.getName() + ": " + e);
+            return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NO_MODIFICATION_ALLOWED_ERR);
+        }
+        finally {
+            try {
+                if (fconn != null)
+                    fconn.close();
+            }
+            catch (IOException e) {
+                Logger.log(FileManager.class.getName() + ": " + e);
+            }
+        }
+        return new PluginResult(PluginResult.Status.OK, fileSize);
+    }
+
+    /**
+     * Returns a directory entry that represents the specified file system. The
+     * directory entry does not represent the root of the file system, but a
+     * directory within the file system that is writable. Users must provide the
+     * file system type, which can be one of FS_TEMPORARY or FS_PERSISTENT.
+     *
+     * @param type
+     *            The type of file system desired.
+     * @param size
+     *            The minimum size, in bytes, of space required
+     * @return a PluginResult containing a file system object for the specified
+     *         file system
+     */
+    protected static PluginResult requestFileSystem(int type, long size) {
+        if (!isValidFileSystemType(type)) {
+            Logger.log(FileManager.class.getName()
+                    + ": Invalid file system type: " + Integer.toString(type));
+            return new PluginResult(
+                    PluginResult.Status.JSON_EXCEPTION,
+                    SYNTAX_ERR);
+        }
+
+        PluginResult result = null;
+        String filePath = null;
+        switch (type) {
+        case FS_TEMPORARY:
+            // create application-specific temp directory
+            try {
+                filePath = FileUtils.createApplicationTempDirectory();
+            }
+            catch (IOException e) {
+                Logger.log(FileManager.class.getName() + ": " + e);
+                return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                        NO_MODIFICATION_ALLOWED_ERR);
+            }
+            break;
+        case FS_PERSISTENT:
+            // get a path to SD card (if present) or user directory (internal)
+            filePath = FileUtils.getFileSystemRoot();
+            break;
+        }
+
+        // create a file system entry from the path
+        Entry entry = null;
+        try {
+            // check the file system size
+            if (size > FileUtils.availableSize(filePath)) {
+                return new PluginResult(
+                        PluginResult.Status.IO_EXCEPTION,
+                        QUOTA_EXCEEDED_ERR);
+            }
+
+            entry = getEntryFromURI(filePath);
+        }
+        catch (Exception e) {
+            // bad path (not likely)
+            return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    ENCODING_ERR);
+        }
+
+        try {
+            JSONObject fileSystem = new JSONObject();
+            fileSystem.put("name", getFileSystemName(type));
+            fileSystem.put("root", entry.toJSONObject());
+            result = new PluginResult(PluginResult.Status.OK, fileSystem);
+        }
+        catch (JSONException e) {
+            return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                    "File system entry JSON conversion failed.");
+        }
+
+        return result;
+    }
+
+    /**
+     * Creates a file system entry object from the specified file system URI.
+     *
+     * @param uri
+     *            the full path to the file or directory on the file system
+     * @return a PluginResult containing the file system entry
+     */
+    protected static PluginResult resolveFileSystemURI(String uri) {
+        PluginResult result = null;
+        Entry entry = null;
+
+        try {
+            entry = getEntryFromURI(uri);
+        }
+        catch (IllegalArgumentException e) {
+            return new PluginResult(
+                    PluginResult.Status.JSON_EXCEPTION,
+                    ENCODING_ERR);
+        }
+
+        if (entry == null) {
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+        else {
+            result = new PluginResult(PluginResult.Status.OK,
+                    entry.toJSONObject());
+        }
+        return result;
+    }
+
+    /**
+     * Retrieve metadata for file or directory specified by path.
+     *
+     * @param path
+     *            full path name of the file or directory
+     * @param full
+     *            return full or partial meta data.
+     * @return PluginResult containing metadata for file system entry or an
+     *         error code if unable to retrieve metadata
+     */
+    protected static PluginResult getMetadata(String path, boolean full) {
+        PluginResult result = null;
+        FileConnection fconn = null;
+        try {
+            fconn = (FileConnection)Connector.open(path);
+            if (fconn.exists()) {
+                if (full) {
+                    JSONObject metadata = new JSONObject();
+                    metadata.put("size", fconn.fileSize());
+                    metadata.put("type",
+                            MIMETypeAssociations.getMIMEType(fconn.getURL()));
+                    metadata.put("name", fconn.getName());
+                    metadata.put("fullPath", fconn.getURL());
+                    metadata.put("lastModifiedDate", fconn.lastModified());
+                    result = new PluginResult(PluginResult.Status.OK, metadata);
+                } else {
+                    result = new PluginResult(PluginResult.Status.OK,
+                            fconn.lastModified());
+                }
+            }
+            else {
+                result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                        NOT_FOUND_ERR);
+            }
+        }
+        catch (IllegalArgumentException e) {
+            // bad path
+            Logger.log(FileUtils.class.getName() + ": " + e);
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+        catch (IOException e) {
+            Logger.log(FileUtils.class.getName() + ": " + e);
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_READABLE_ERR);
+        }
+        catch (JSONException e) {
+            result = new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                    "File system entry JSON conversion failed.");
+        }
+        finally {
+            try {
+                if (fconn != null) fconn.close();
+            }
+            catch (IOException ignored) {
+            }
+        }
+        return result;
+    }
+
+    private static JSONObject buildEntry(String dirPath, String filePath) throws JSONException {
+        JSONObject entry = new JSONObject();
+        boolean isDir = filePath.endsWith(FileUtils.FILE_SEPARATOR);
+
+        entry.put("isFile", !isDir);
+        entry.put("isDirectory", isDir);
+        entry.put("name", isDir ? filePath.substring(0, filePath.length()-1) : filePath);
+        entry.put("fullPath", dirPath + filePath);
+
+        return entry;
+    }
+
+    /**
+     * Returns a listing of the specified directory contents. Names of both
+     * files and directories are returned.
+     *
+     * @param path
+     *            full path name of directory
+     * @return PluginResult containing list of file and directory names
+     *         corresponding to directory contents
+     */
+    protected static PluginResult listDirectory(String path) {
+        Enumeration listing = null;
+        try {
+            listing = FileUtils.listDirectory(path);
+        }
+        catch (Exception e) {
+            // bad path
+            Logger.log(FileUtils.class.getName() + ": " + e);
+            return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                    NOT_FOUND_ERR);
+        }
+
+        try {
+            // pass directory contents back as an array of JSONObjects (entries)
+            JSONArray array = new JSONArray();
+            while (listing.hasMoreElements()) {
+                array.add(buildEntry(path, (String) listing.nextElement()));
+            }
+
+            return new PluginResult(PluginResult.Status.OK, array);
+        } catch (JSONException e) {
+            return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                    "File system entry JSON conversion failed.");
+        }
+    }
+
+    /**
+     * Copies a file or directory to a new location. If copying a directory, the
+     * entire contents of the directory are copied recursively.
+     *
+     * @param srcPath
+     *            the full path of the file or directory to be copied
+     * @param parent
+     *            the full path of the target directory to which the file or
+     *            directory should be copied
+     * @param newName
+     *            the new name of the file or directory
+     * @return PluginResult containing an Entry object representing the new
+     *         entry, or an error code if an error occurs
+     */
+    protected static PluginResult copyTo(String srcPath, String parent, String newName) {
+        try {
+            FileUtils.copy(srcPath, parent, newName);
+        }
+        catch (IllegalArgumentException e) {
+            Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+            return new PluginResult(
+                    PluginResult.Status.JSON_EXCEPTION, ENCODING_ERR);
+        }
+        catch (FileNotFoundException e) {
+            Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+            return new PluginResult(
+                    PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+        }
+        catch (SecurityException e) {
+            Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+            return new PluginResult(
+                    PluginResult.Status.IO_EXCEPTION, SECURITY_ERR);
+        }
+        catch (IOException e) {
+            Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+            return new PluginResult(
+                    PluginResult.Status.IO_EXCEPTION, INVALID_MODIFICATION_ERR);
+        }
+
+        return resolveFileSystemURI(getFullPath(parent, newName));
+    }
+
+    /**
+     * Moves a file or directory to a new location. If moving a directory, the
+     * entire contents of the directory are moved recursively.
+     * <p>
+     * It is an error to try to: move a directory inside itself; move a
+     * directory into its parent unless the name has changed; move a file to a
+     * path occupied by a directory; move a directory to a path occupied by a
+     * file; move any element to a path occupied by a directory that is not
+     * empty.
+     * </p>
+     * <p>
+     * A move of a file on top of an existing file must attempt to delete and
+     * replace that file. A move of a directory on top of an existing empty
+     * directory must attempt to delete and replace that directory.
+     * </p>
+     *
+     * @param srcPath
+     *            the full path of the file or directory to be moved
+     * @param parent
+     *            the full path of the target directory to which the file or
+     *            directory should be copied
+     * @param newName
+     *            the new name of the file or directory
+     * @return PluginResult containing an Entry object representing the new
+     *         entry, or an error code if an error occurs
+     */
+    protected static PluginResult moveTo(String srcPath, String parent, String newName) {
+
+        // check paths
+        if (parent == null || newName == null) {
+            Logger.log(FileManager.class.getName() + ": Parameter cannot be null.");
+            return new PluginResult(
+                    PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+        }
+        else if (!parent.endsWith(FileUtils.FILE_SEPARATOR)) {
+            parent += FileUtils.FILE_SEPARATOR;
+        }
+
+        // Rules:
+        // 1 - file replace existing file ==> OK
+        // 2 - directory replace existing EMPTY directory ==> OK
+        // 3 - file replace existing directory ==> NO!
+        // 4 - directory replace existing file ==> NO!
+        // 5 - ANYTHING replace non-empty directory ==> NO!
+        //
+        // The file-to-directory and directory-to-file checks are performed in
+        // the copy operation (below). In addition, we check the destination
+        // path to see if it is a directory that is not empty. Also, if the
+        // source and target paths have the same parent directory, it is far
+        // more efficient to rename the source.
+        //
+        FileConnection src = null;
+        FileConnection dst = null;
+        try {
+            src = (FileConnection)Connector.open(srcPath, Connector.READ_WRITE);
+            if (!src.exists()) {
+                Logger.log(FileManager.class.getName() + ": Path not found: " + srcPath);
+                return new PluginResult(
+                        PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+            }
+
+            if (src.isDirectory() && !srcPath.endsWith(FileUtils.FILE_SEPARATOR)) {
+                // Rename of a directory on OS 7+ is quirky in that it requires
+                // the opened file path to have a trailing slash.
+                src.close();
+                src = (FileConnection)Connector.open(srcPath + '/', Connector.READ_WRITE);
+            }
+
+            // cannot delete the destination path if it is a directory that is
+            // not empty
+            dst = (FileConnection) Connector.open(parent + newName, Connector.READ_WRITE);
+            if (dst.isDirectory() && dst.list("*", true).hasMoreElements()) {
+                return new PluginResult(
+                        PluginResult.Status.IO_EXCEPTION, INVALID_MODIFICATION_ERR);
+            }
+
+            // simply rename if source path and parent are same directory
+            String srcURL = src.getURL();
+            String srcName = src.getName();
+            String srcDir = srcURL.substring(0, srcURL.length() - srcName.length());
+            if (srcDir.equals(parent)) {
+                // rename to itself is an error
+                if (FileUtils.stripSeparator(srcName).equals(
+                        FileUtils.stripSeparator(newName))) {
+                    return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+                            INVALID_MODIFICATION_ERR);
+                }
+
+                // file replace file || directory replace directory ==> OK
+                // delete the existing entry
+                if (dst.exists() &&
+                        ( (src.isDirectory() && dst.isDirectory()) ||
+                          (!src.isDirectory() && !dst.isDirectory()) )) {
+                    dst.delete();
+                }
+
+                // rename
+                src.rename(newName);
+                Entry entry = getEntryFromURI(parent + newName);
+                return new PluginResult(PluginResult.Status.OK, entry.toJSONObject());
+            }
+        }
+        catch (IllegalArgumentException e) {
+            Logger.log(FileManager.class.getName() + ": " + e);
+            return new PluginResult(
+                    PluginResult.Status.JSON_EXCEPTION,
+                    ENCODING_ERR);
+        }
+        catch (IOException e) {
+            // rename failed
+            Logger.log(FileManager.class.getName() + ": " + e);
+            return new PluginResult(
+                    PluginResult.Status.IO_EXCEPTION,
+                    INVALID_MODIFICATION_ERR);
+        }
+        finally {
+            try {
+                if (src != null) src.close();
+                if (dst != null) dst.close();
+            }
+            catch (IOException ignored) {
+            }
+        }
+
+        // There is no FileConnection API to move files and directories, so
+        // the move is a copy operation from source to destination, followed by
+        // a delete operation of the source.
+        //
+        // The following checks are made in the copy operation:
+        //   * moving a directory into itself,
+        //   * moving a file to an existing directory, and
+        //   * moving a directory to an existing file
+        //
+        // copy source to destination
+        PluginResult result = copyTo(srcPath, parent, newName);
+
+        // if copy succeeded, delete source
+        if (result.getStatus() == PluginResult.Status.OK.ordinal()) {
+            try {
+                FileUtils.delete(srcPath);
+            }
+            catch (IOException e) {
+                // FIXME: half of move failed, but deleting either source or
+                // destination to compensate seems risky
+                Logger.log(FileManager.class.getName()
+                        + ": Failed to delete source directory during move operation.");
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Creates a file system entry for the file or directory located at the
+     * specified path.
+     *
+     * @param filePath
+     *            full path name of an entry on the file system
+     * @return a file system entry corresponding to the file path, or
+     *         <code>null</code> if the path is invalid or does not exist on the
+     *         file system
+     * @throws IllegalArgumentException
+     *             is the file path is invalid
+     * @throws IOException
+     */
+    protected static Entry getEntryFromURI(String filePath)
+            throws IllegalArgumentException {
+        // check for bogus path
+        String path = (filePath == null) ? null : filePath.trim();
+        if (path == null || path.length() < 1) {
+            throw new IllegalArgumentException("Invalid URI.");
+        }
+
+        // create a file system entry
+        Entry entry = null;
+        if (path.startsWith(FileUtils.LOCAL_PROTOCOL)) {
+            entry = getEntryFromLocalURI(filePath);
+        }
+        else {
+            FileConnection fconn = null;
+            try {
+                fconn = (FileConnection) Connector.open(path);
+                if (fconn.exists()) {
+                    // create a new Entry
+                    entry = new Entry();
+                    entry.setDirectory(fconn.isDirectory());
+                    entry.setName(FileUtils.stripSeparator(fconn.getName()));
+                    entry.setFullPath(FileUtils.stripSeparator(path));
+                }
+            }
+            catch (IOException e) {
+                Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+            }
+            finally {
+                try {
+                    if (fconn != null) fconn.close();
+                }
+                catch (IOException ignored) {
+                }
+            }
+        }
+
+        return entry;
+    }
+
+    /**
+     * Creates a file system entry for a resource contained in the packaged
+     * application. Use this method if the specified path begins with
+     * <code>local:///</code> protocol.
+     *
+     * @param localPath
+     *            the path of the application resource
+     * @return a file system entry corresponding to the local path, or
+     *         <code>null</code> if a resource does not exist at the specified
+     *         path
+     */
+    private static Entry getEntryFromLocalURI(String localPath) {
+        // Remove local:// from filePath but leave a leading /
+        String path = localPath.substring(8);
+        Entry entry = null;
+        if (FileUtils.FILE_SEPARATOR.equals(path)
+                || Application.class.getResourceAsStream(path) != null) {
+            entry = new Entry();
+            entry.setName(path.substring(1));
+            entry.setFullPath(localPath);
+        }
+        return entry;
+    }
+
+    /**
+     * Tests whether the specified file system type is valid.
+     *
+     * @param type
+     *            file system type
+     * @return true if file system type is valid
+     */
+    protected static boolean isValidFileSystemType(int type) {
+        return (type == FS_TEMPORARY || type == FS_PERSISTENT);
+    }
+
+    /**
+     * Determines if the specified path is the root path of a file system.
+     *
+     * @param path
+     *            full path
+     * @return true if the path is the root path of a file system
+     */
+    protected static boolean isFileSystemRoot(String path) {
+        if (path == null) {
+            return false;
+        }
+
+        if (!path.endsWith(FileUtils.FILE_SEPARATOR)) {
+            path += FileUtils.FILE_SEPARATOR;
+        }
+
+        boolean isRoot = false;
+        Enumeration e = FileSystemRegistry.listRoots();
+        while (e.hasMoreElements()) {
+            String root = "file:///" + (String) e.nextElement();
+            if (root.equals(path)) {
+                isRoot = true;
+                break;
+            }
+        }
+
+        return (isRoot || path.equals(FileUtils.getApplicationTempDirPath()));
+    }
+
+    /**
+     * Retrieves the name for the specified file system type.
+     *
+     * @param type
+     *            file system type
+     * @return file system name
+     */
+    protected static String getFileSystemName(int type) {
+        String name = null;
+        switch (type) {
+        case FS_TEMPORARY:
+            name = "temporary";
+            break;
+        case FS_PERSISTENT:
+            name = "persistent";
+            break;
+        }
+        return name;
+    }
+
+    /**
+     * Returns full path from the directory and name specified.
+     *
+     * @param parent
+     *            full path of the parent directory
+     * @param name
+     *            name of the directory entry (can be <code>null</code>)
+     * @return full path of the file system entry
+     * @throws IllegalArgumentException
+     *             if <code>parent</code> is <code>null</code>
+     */
+    public static String getFullPath(String parent, String name)
+            throws IllegalArgumentException {
+        if (parent == null) {
+            throw new IllegalArgumentException("Directory cannot be null.");
+        }
+
+        if (!parent.endsWith(FileUtils.FILE_SEPARATOR)) {
+            parent += FileUtils.FILE_SEPARATOR;
+        }
+        return (name == null) ? parent : parent + name;
+    }
+
+    /**
+     * Determines if the specified action should be run synchronously.
+     *
+     * @param action
+     *            the action to perform
+     * @return true if the action should be synchronous
+     */
+    public boolean isSynch(String action) {
+        if (ACTION_IS_FILE_SYSTEM_ROOT.equals(action)) {
+            return true;
+        }
+        return super.isSynch(action);
+    }
+}