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

[26/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/camera/Camera.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/Camera.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/Camera.java
new file mode 100644
index 0000000..d54483f
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/Camera.java
@@ -0,0 +1,470 @@
+/*
+ * 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.camera;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Date;
+
+import javax.microedition.io.Connector;
+import javax.microedition.io.file.FileConnection;
+
+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.util.Logger;
+
+import net.rim.blackberry.api.invoke.CameraArguments;
+import net.rim.blackberry.api.invoke.Invoke;
+import net.rim.device.api.io.Base64OutputStream;
+import net.rim.device.api.io.IOUtilities;
+import net.rim.device.api.system.ApplicationDescriptor;
+import net.rim.device.api.system.Bitmap;
+import net.rim.device.api.system.Characters;
+import net.rim.device.api.system.ControlledAccessException;
+import net.rim.device.api.system.EncodedImage;
+import net.rim.device.api.system.EventInjector;
+import net.rim.device.api.system.JPEGEncodedImage;
+import net.rim.device.api.system.PNGEncodedImage;
+import net.rim.device.api.ui.UiApplication;
+
+/**
+ * The Camera plugin interface.
+ *
+ * The Camera class can invoke the following actions:
+ *
+ *   - takePicture: takes photo and returns base64 encoded image or image file URI
+ *
+ *   future?
+ *   - captureVideo...
+ *
+ */
+public class Camera extends Plugin
+{
+    /**
+     * Possible actions.
+     */
+    public static final String ACTION_TAKE_PICTURE = "takePicture";
+
+    /**
+     * Maximum image encoding size (in bytes) to allow.  (Obtained unofficially
+     * through trial and error). Anything larger will cause stability issues
+     * when sending back to the browser.
+     */
+    private static final long MAX_ENCODING_SIZE = 1500000L;
+
+    /**
+     * 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;
+
+        // take a picture
+        if (action != null && action.equals(ACTION_TAKE_PICTURE))
+        {
+            // Parse the options specified for the take picture action.
+            CameraOptions options;
+            try {
+                options = CameraOptions.fromJSONArray(args);
+            } catch (NumberFormatException e) {
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION, "One of the camera options is not a valid number.");
+            } catch (JSONException e) {
+                return new PluginResult(PluginResult.Status.JSON_EXCEPTION, "One of the camera options is not valid JSON.");
+            }
+
+            // launch native camera application
+            launchCamera(new PhotoListener(options, callbackId));
+
+            // The native camera application runs in a separate process, so we
+            // must now wait for the listener to retrieve the photo taken.
+            // Return NO_RESULT status so plugin manager does not invoke a callback,
+            // but keep the callback so the listener can invoke it later.
+            result = new PluginResult(PluginResult.Status.NO_RESULT);
+            result.setKeepCallback(true);
+            return result;
+        }
+        else
+        {
+            result = new PluginResult(PluginResult.Status.INVALID_ACTION, "Camera: Invalid action:" + action);
+        }
+
+        return result;
+    }
+
+    /**
+     * Launches the native camera application.
+     */
+    private static void launchCamera(PhotoListener listener)
+    {
+        // MMAPI interface doesn't use the native Camera application or interface
+        // (we would have to replicate it).  So, we invoke the native Camera application,
+        // which doesn't allow us to set any options.
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().addFileSystemJournalListener(listener);
+            Invoke.invokeApplication(Invoke.APP_TYPE_CAMERA, new CameraArguments());
+        }
+    }
+
+    /**
+     * Closes the native camera application.
+     */
+    public static void closeCamera()
+    {
+        // simulate two escape characters to exit native camera application
+        // no, there is no other way to do this
+        UiApplication.getUiApplication().invokeLater(new Runnable() {
+            public void run() {
+                try
+                {
+                    EventInjector.KeyEvent inject = new EventInjector.KeyEvent(
+                            EventInjector.KeyEvent.KEY_DOWN, Characters.ESCAPE, 0);
+                    inject.post();
+                    inject.post();
+                }
+                catch (ControlledAccessException e)
+                {
+                    // the application doesn't have key injection permissions
+                    Logger.log(Camera.class.getName() + ": Unable to close camera.  " +
+                            ApplicationDescriptor.currentApplicationDescriptor().getName() +
+                            " does not have key injection permissions.");
+                }
+            }
+        });
+    }
+
+    /**
+     * Returns the image file URI or the Base64-encoded image.
+     * @param filePath The full path of the image file
+     * @param options Specifies the format of the image and the result
+     * @param callbackId The id of the callback to receive the result
+     */
+    public static void processImage(String filePath, CameraOptions options,
+            String callbackId) {
+        PluginResult result = null;
+        try
+        {
+            // wait for the file to be fully written to the file system
+            // to avoid premature access to it (yes, this has happened)
+            waitForImageFile(filePath);
+
+            // Reformat the image if the specified options require it,
+            // otherwise, get encoded string if base 64 string is output format.
+            String imageURIorData = filePath;
+            
+            // save to file:///store/home/user/ as oppsed to photo album
+            // so it doesn't show up in the camera's photo album viewer
+            if(!options.saveToPhotoAlbum){
+                FileConnection fconnIn = null;
+                FileConnection fconnOut = null;
+                InputStream in = null;
+                OutputStream out = null;
+                String newOutName = "";
+                try
+                {
+                    fconnIn = (FileConnection)Connector.open(filePath);
+                    if (fconnIn.exists())
+                    {
+                        newOutName = "file:///store/home/user/"+fconnIn.getName();
+                        fconnOut = (FileConnection)Connector.open(newOutName);
+                        if (!fconnOut.exists())
+                         {
+                             fconnOut.create();  
+                             in = fconnIn.openInputStream();
+                             out = fconnOut.openOutputStream();
+                             out.write(IOUtilities.streamToBytes(in, 96*1024));
+                             fconnIn.delete();
+                             out.close();
+                             imageURIorData = newOutName;
+                             filePath = newOutName;
+                             waitForImageFile(newOutName);
+                         }
+                    }
+                }
+                finally
+                {
+                    if (in != null) in.close();
+                    if (out != null) out.close();
+                    if (fconnIn != null) fconnIn.close();
+                    if (fconnOut != null) fconnOut.close();
+                }
+                
+            }
+
+            if (options.reformat) {
+                imageURIorData = reformatImage(filePath, options);
+            } else if (options.destinationType == CameraOptions.DESTINATION_DATA_URL) {
+                imageURIorData = encodeImage(filePath);
+            }
+
+            // we have to check the size to avoid memory errors in the browser
+            if (imageURIorData.length() > MAX_ENCODING_SIZE)
+            {
+                // it's a big one.  this is for your own good.
+                String msg = "Encoded image is too large.  Try reducing camera image size.";
+                Logger.log(Camera.class.getName() + ": " + msg);
+                result =  new PluginResult(PluginResult.Status.ERROR, msg);
+            }
+            else
+            {
+                result = new PluginResult(PluginResult.Status.OK, imageURIorData);
+            }
+        }
+        catch (Exception e)
+        {
+            result = new PluginResult(PluginResult.Status.IO_EXCEPTION, e.toString());
+        }
+
+        // send result back to JavaScript
+        sendResult(result, callbackId);
+    }
+
+    /**
+     * Waits for the image file to be fully written to the file system.
+     * @param filePath     Full path of the image file
+     * @throws IOException
+     */
+    private static void waitForImageFile(String filePath) throws IOException
+    {
+        long start = (new Date()).getTime();
+        FileConnection fconn = null;
+        try
+        {
+            fconn = (FileConnection)Connector.open(filePath, Connector.READ);
+            if (fconn.exists())
+            {
+                long fileSize = fconn.fileSize();
+                long size = 0;
+                while (true)
+                {
+                    try { Thread.sleep(100); } catch (InterruptedException e) {}
+                    size = fconn.fileSize();
+                    if (size == fileSize) {
+                        break;
+                    }
+                    fileSize = size;
+                }
+                Logger.log(Camera.class.getName() + ": " + filePath +
+                    " size=" + Long.toString(fileSize) + " bytes");
+            }
+        }
+        finally
+        {
+            if (fconn != null) fconn.close();
+        }
+        long end = (new Date()).getTime();
+        Logger.log(Camera.class.getName() + ": wait time=" + Long.toString(end-start) + " ms");
+    }
+
+    /**
+     * Opens the specified image file and converts its contents to a Base64-encoded string.
+     * @param filePath     Full path of the image file
+     * @return file contents as a Base64-encoded String
+     */
+    private static String encodeImage(String filePath) throws IOException
+    {
+        String imageData = null;
+
+        // open the image file
+        FileConnection fconn = null;
+        InputStream in = null;
+        ByteArrayOutputStream byteArrayOS = null;
+        try
+        {
+            fconn = (FileConnection)Connector.open(filePath);
+            if (fconn.exists())
+            {
+                // encode file contents using BASE64 encoding
+                in = fconn.openInputStream();
+                byteArrayOS = new ByteArrayOutputStream();
+                Base64OutputStream base64OS = new Base64OutputStream(byteArrayOS);
+                base64OS.write(IOUtilities.streamToBytes(in, 96*1024));
+                base64OS.flush();
+                base64OS.close();
+                imageData = byteArrayOS.toString();
+
+                Logger.log(Camera.class.getName() + ": Base64 encoding size=" +
+                        Integer.toString(imageData.length()));
+            }
+        }
+        finally
+        {
+            if (in != null) in.close();
+            if (fconn != null) fconn.close();
+            if (byteArrayOS != null) byteArrayOS.close();
+        }
+
+        return imageData;
+    }
+
+    /**
+     * Reformats the image taken with the camera based on the options specified.
+     *
+     * Unfortunately, reformatting the image will cause EXIF data in the photo
+     * to be lost.  Most importantly the orientation data is lost so the
+     * picture is not auto rotated by software that recognizes EXIF data.
+     *
+     * @param filePath
+     *            The full path of the image file
+     * @param options
+     *            Specifies the format of the image and the result
+     * @return the reformatted image file URI or Base64-encoded image
+     * @throws IOException
+     */
+    private static String reformatImage(String filePath, CameraOptions options)
+            throws IOException {
+        long start = (new Date()).getTime();
+
+        // Open the original image created by the camera application and read
+        // it into an EncodedImage object.
+        FileConnection fconn = null;
+        InputStream in = null;
+        Bitmap originalImage = null;
+        try {
+            fconn = (FileConnection) Connector.open(filePath);
+            in = fconn.openInputStream();
+            originalImage = Bitmap.createBitmapFromBytes(IOUtilities.streamToBytes(in, 96*1024), 0, -1, 1);
+        } finally {
+            if (in != null)
+                in.close();
+            if (fconn != null)
+                fconn.close();
+        }
+
+        int newWidth = options.targetWidth;
+        int newHeight = options.targetHeight;
+        int origWidth = originalImage.getWidth();
+        int origHeight = originalImage.getHeight();
+
+        // If only width or only height was specified, the missing dimension is
+        // set based on the current aspect ratio of the image.
+        if (newWidth > 0 && newHeight <= 0) {
+            newHeight = (newWidth * origHeight) / origWidth;
+        } else if (newWidth <= 0 && newHeight > 0) {
+            newWidth = (newHeight * origWidth) / origHeight;
+        } else if (newWidth <= 0 && newHeight <= 0) {
+            newWidth = origWidth;
+            newHeight = origHeight;
+        } else {
+            // If the user specified both a positive width and height
+            // (potentially different aspect ratio) then the width or height is
+            // scaled so that the image fits while maintaining aspect ratio.
+            // Alternatively, the specified width and height could have been
+            // kept and Bitmap.SCALE_TO_FIT specified when scaling, but this
+            // would result in whitespace in the new image.
+            double newRatio = newWidth / (double)newHeight;
+            double origRatio = origWidth / (double)origHeight;
+
+            if (origRatio > newRatio) {
+                newHeight = (newWidth * origHeight) / origWidth;
+            } else if (origRatio < newRatio) {
+                newWidth = (newHeight * origWidth) / origHeight;
+            }
+        }
+
+        Bitmap newImage = new Bitmap(newWidth, newHeight);
+        originalImage.scaleInto(newImage, options.imageFilter, Bitmap.SCALE_TO_FILL);
+
+        // Convert the image to the appropriate encoding.  PNG does not allow
+        // quality to be specified so the only affect that the quality option
+        // has for a PNG is on the seelction of the image filter.
+        EncodedImage encodedImage;
+        if (options.encoding == CameraOptions.ENCODING_PNG) {
+            encodedImage = PNGEncodedImage.encode(newImage);
+        } else {
+            encodedImage = JPEGEncodedImage.encode(newImage, options.quality);
+        }
+
+        // Rewrite the modified image back out to the same file.  This is done
+        // to ensure that for every picture taken, only one shows up in the
+        // gallery.  If the encoding changed the file extension will differ
+        // from the original.
+        OutputStream out = null;
+        int dirIndex = filePath.lastIndexOf('/');
+        String filename = filePath.substring(dirIndex + 1, filePath.lastIndexOf('.'))
+                + options.fileExtension;
+        try {
+            fconn = (FileConnection) Connector.open(filePath);
+            fconn.truncate(0);
+            out = fconn.openOutputStream();
+            out.write(encodedImage.getData());
+            fconn.rename(filename);
+        } finally {
+            if (out != null)
+                out.close();
+            if (fconn != null)
+                fconn.close();
+        }
+
+        // Return either the Base64-encoded string or the image URI for the
+        // new image.
+        String imageURIorData;
+        if (options.destinationType == CameraOptions.DESTINATION_DATA_URL) {
+            ByteArrayOutputStream byteArrayOS = null;
+
+            try {
+                byteArrayOS = new ByteArrayOutputStream();
+                Base64OutputStream base64OS = new Base64OutputStream(
+                        byteArrayOS);
+                base64OS.write(encodedImage.getData());
+                base64OS.flush();
+                base64OS.close();
+                imageURIorData = byteArrayOS.toString();
+                Logger.log(Camera.class.getName() + ": Base64 encoding size="
+                        + Integer.toString(imageURIorData.length()));
+            } finally {
+                if (byteArrayOS != null) {
+                    byteArrayOS.close();
+                }
+            }
+        } else {
+            imageURIorData = filePath.substring(0, dirIndex + 1) + filename;
+        }
+
+        long end = (new Date()).getTime();
+        Logger.log(Camera.class.getName() + ": reformat time=" + Long.toString(end-start) + " ms");
+
+        return imageURIorData;
+    }
+
+    /**
+     * Sends result back to JavaScript.
+     * @param result PluginResult
+     */
+    private static void sendResult(PluginResult result, String callbackId)
+    {
+        // invoke the appropriate callback
+        if (result.getStatus() == PluginResult.Status.OK.ordinal())
+        {
+            success(result, callbackId);
+        }
+        else
+        {
+            error(result, callbackId);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/CameraOptions.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/CameraOptions.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/CameraOptions.java
new file mode 100644
index 0000000..8bfa0df
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/CameraOptions.java
@@ -0,0 +1,193 @@
+/*
+ * 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.camera;
+
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+
+import net.rim.device.api.system.Bitmap;
+
+/**
+ * A helper class to hold all the options specified when using the camera api.
+ */
+public class CameraOptions {
+
+    /** Return the result as a Base-64 encoded string. */
+    public static final int DESTINATION_DATA_URL = 0;
+
+    /** Return the result as a file URI. */
+    public static final int DESTINATION_FILE_URI = 1;
+
+    /** JPEG image encoding. */
+    public static final int ENCODING_JPEG = 0;
+
+    /** PNG image encoding. */
+    public static final int ENCODING_PNG = 1;
+
+    /** Select image from picture library. */
+    public static final int SOURCE_PHOTOLIBRARY = 0;
+
+    /** Take picture from camera. */
+    public static final int SOURCE_CAMERA = 1;
+
+    /** Select image from picture library. */
+    public static final int SOURCE_SAVEDPHOTOALBUM = 2;
+
+    // Class members with defaults set.
+    public int quality = 80;
+    public int destinationType = DESTINATION_DATA_URL;
+    public int sourceType = SOURCE_CAMERA;
+    public int targetWidth = -1;
+    public int targetHeight = -1;
+    public int encoding = ENCODING_JPEG;
+    public String fileExtension = ".jpg";
+    public int imageFilter = Bitmap.FILTER_LANCZOS;
+    public boolean reformat = false;
+    public boolean saveToPhotoAlbum = true;
+
+    /**
+     * Defines the order of args in the JSONArray
+     *
+     * [ 80,                                   // quality
+     *   Camera.DestinationType.DATA_URL,      // destinationType
+     *   Camera.PictureSourceType.PHOTOLIBRARY // sourceType (ignored)
+     *   400,                                  // targetWidth
+     *   600,                                  // targetHeight
+     *   Camera.EncodingType.JPEG              // encoding
+     *	 Camera.mediaType
+     *   Camera.allowEdit
+     *   Camera.correctOrientation
+     *	 Camera.saveToPhotoAlbum			   // save to photo album
+     *   Camera.popoverOptions]			   
+     */
+    private static final int ARG_QUALITY = 0;
+    private static final int ARG_DESTINATION_TYPE = 1;
+    private static final int ARG_SOURCE_TYPE = 2;
+    private static final int ARG_TARGET_WIDTH = 3;
+    private static final int ARG_TARGET_HEIGHT = 4;
+    private static final int ARG_ENCODING = 5;
+    private static final int ARG_SAVETOPHOTOALBUM = 9;
+
+    /**
+     * Parse the JSONArray and populate the class members with the values.
+     *
+     * @param args
+     *            a JSON Array of camera options.
+     * @return a new CameraOptions object with values set.
+     * @throws NumberFormatException
+     * @throws JSONException
+     */
+    public static CameraOptions fromJSONArray(JSONArray args)
+            throws NumberFormatException, JSONException {
+        CameraOptions options = new CameraOptions();
+
+        if (args != null && args.length() > 0) {
+            // Use the quality value to determine what image filter to use
+            // if a reformat is necessary.  The possible values in order from
+            // fastest (poorest quality) to slowest (best quality) are:
+            //
+            //     FILTER_BOX -> FILTER_BILINEAR -> FILTER_LANCZOS
+            if (!args.isNull(ARG_QUALITY)) {
+                int quality = Integer.parseInt(args.getString(ARG_QUALITY));
+                if (quality > 0) {
+                    options.quality = quality > 100 ? 100 : quality;
+                    if (options.quality < 30) {
+                        options.imageFilter = Bitmap.FILTER_BOX;
+                    } else if (options.quality < 60) {
+                        options.imageFilter = Bitmap.FILTER_BILINEAR;
+                    }
+                }
+            }
+
+            if (!args.isNull(ARG_DESTINATION_TYPE)) {
+                int destType = Integer.parseInt(args
+                        .getString(ARG_DESTINATION_TYPE));
+                if (destType == DESTINATION_FILE_URI) {
+                    options.destinationType = DESTINATION_FILE_URI;
+                }
+            }
+
+            if (!args.isNull(ARG_SOURCE_TYPE)) {
+                options.sourceType = Integer.parseInt(args
+                        .getString(ARG_SOURCE_TYPE));
+            }
+
+            if (!args.isNull(ARG_TARGET_WIDTH)) {
+                options.targetWidth = Integer.parseInt(args
+                        .getString(ARG_TARGET_WIDTH));
+            }
+
+            if (!args.isNull(ARG_TARGET_HEIGHT)) {
+                options.targetHeight = Integer.parseInt(args
+                        .getString(ARG_TARGET_HEIGHT));
+            }
+
+            if (!args.isNull(ARG_ENCODING)) {
+                int encoding = Integer.parseInt(args.getString(ARG_ENCODING));
+                if (encoding == ENCODING_PNG) {
+                    options.encoding = ENCODING_PNG;
+                    options.fileExtension = ".png";
+                }
+            }
+
+            // A reformat of the picture taken from the camera is only performed
+            // if a custom width or height was specified or the user wants
+            // the output in an encoded form which is not JPEG.
+            if (options.targetWidth > 0 || options.targetHeight > 0
+                    || options.encoding != ENCODING_JPEG) {
+                options.reformat = true;
+            }
+
+            if (!args.isNull(ARG_SAVETOPHOTOALBUM)) {
+                options.saveToPhotoAlbum = parseBoolean(args.getString(ARG_SAVETOPHOTOALBUM));
+            }
+            
+        }
+
+        return options;
+    }
+
+    /**
+     * no parseBoolean in JDK 1.3 :(
+    */
+    public static boolean parseBoolean(String s) {
+        if(s.equals("true")){
+            return true;
+        }else{
+            return false;
+        }
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+        str.append("Destination: " + destinationType + "\n");
+        str.append("Source: " + sourceType + "\n");
+        str.append("Quality: " + quality + "\n");
+        str.append("Width:  " + targetWidth + "\n");
+        str.append("Height: " + targetHeight + "\n");
+        str.append("Encoding:    " + encoding + "\n");
+        str.append("Filter: " + imageFilter + "\n");
+        str.append("Reformat: " + reformat);
+        str.append("Save To Photo Album: " + saveToPhotoAlbum);
+        return str.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/PhotoListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/PhotoListener.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/PhotoListener.java
new file mode 100644
index 0000000..8571788
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/camera/PhotoListener.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.camera;
+
+import net.rim.device.api.io.file.FileSystemJournal;
+import net.rim.device.api.io.file.FileSystemJournalEntry;
+import net.rim.device.api.io.file.FileSystemJournalListener;
+import net.rim.device.api.ui.UiApplication;
+
+/**
+ * Listens for photo added to file system and invokes the specified callback
+ * with the result formatted according the specified destination type.
+ */
+public class PhotoListener implements FileSystemJournalListener {
+
+    /**
+     * Image format options specified by the caller.
+     */
+    private CameraOptions options;
+
+    /**
+     * Callback to be invoked with the result.
+     */
+    private String callbackId;
+
+    /**
+     * Used to track file system changes.
+     */
+    private long lastUSN = 0;
+
+    /**
+     * Constructor.
+     * @param options         Specifies the format of the image and result
+     * @param callbackId      The id of the callback to receive the result
+     */
+    public PhotoListener(CameraOptions options, String callbackId)
+    {
+        this.options = options;
+        this.callbackId = callbackId;
+    }
+
+    /**
+     * Listens for file system changes.  When a JPEG file is added, we process
+     * it and send it back.
+     */
+    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;
+            }
+
+            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED)
+            {
+                String path = entry.getPath();
+                if (path != null && path.indexOf(".jpg") != -1)
+                {
+                    // we found a new JPEG file
+                    // first, stop listening to avoid processing the file more than once
+                    synchronized(UiApplication.getEventLock()) {
+                        UiApplication.getUiApplication().removeFileSystemJournalListener(this);
+                    }
+
+                    // process the image on a background thread to avoid clogging the event queue
+                    final String filePath = "file://" + path;
+                    Thread thread = new Thread(new Runnable() {
+                        public void run() {
+                            Camera.processImage(filePath, options, callbackId);
+                        }
+                    });
+                    thread.start();
+
+                    // clean up
+                    Camera.closeCamera();
+
+                    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/AudioCaptureListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureListener.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureListener.java
new file mode 100644
index 0000000..9267e9b
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureListener.java
@@ -0,0 +1,80 @@
+/*
+ * 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 audio recording files that are added to file system.
+ * <p>
+ * Audio recordings are added to the file system when the user stops the
+ * recording. The audio recording file extension is '.amr'. Therefore, we listen
+ * for the <code>FileSystemJournalEntry.FILE_ADDED</code> event, capturing when
+ * the new file is written.
+ * <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.
+ */
+public class AudioCaptureListener implements FileSystemJournalListener {
+    /**
+     * Used to track file system changes.
+     */
+    private long lastUSN = 0;
+
+    /**
+     * Queue to send media files to for processing.
+     */
+    private MediaQueue queue = null;
+
+    /**
+     * Constructor.
+     */
+    AudioCaptureListener(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;
+            }
+
+            // has audio recording file has been added to the file system?
+            String path = entry.getPath();
+            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED
+                    && path.endsWith(".amr")) {
+                // add file path to the capture queue
+                queue.add("file://" + path);
+
+                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/AudioCaptureOperation.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureOperation.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureOperation.java
new file mode 100644
index 0000000..f4fd9b4
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioCaptureOperation.java
@@ -0,0 +1,173 @@
+/*
+ * 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 java.util.Date;
+
+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 AudioCaptureOperation extends CaptureOperation {
+
+    // content type
+    public static final String CONTENT_TYPE = "audio/";
+
+    // maximum duration to capture media (milliseconds)
+    private double duration = 0;
+
+    // file system listener
+    private AudioCaptureListener listener = null;
+
+    /**
+     * Creates and starts an audio capture operation.
+     *
+     * @param limit
+     *            maximum number of media files to capture
+     * @param duration
+     *            maximum duration to capture media (milliseconds)
+     * @param callbackId
+     *            the callback to receive the files
+     * @param queue
+     *            the queue from which to retrieve captured media files
+     */
+    public AudioCaptureOperation(long limit, double duration, String callbackId, MediaQueue queue) {
+        super(limit, callbackId, queue);
+
+        if (duration > 0) {
+            this.duration = duration;
+        }
+
+        // listener to capture image files added to file system
+        this.listener = new AudioCaptureListener(queue);
+
+        start();
+    }
+
+    /**
+     * Registers file system listener and launches native voice notes recorder
+     * application.
+     */
+    protected void setup() {
+        // register listener for files being written
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().addFileSystemJournalListener(listener);
+        }
+
+        // launch the native voice notes recorder application
+        AudioControl.launchAudioRecorder();
+    }
+
+    /**
+     * Unregisters file system listener and closes native voice notes recorder
+     * application.
+     */
+    protected void teardown() {
+        // remove file system listener
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().removeFileSystemJournalListener(listener);
+        }
+
+        // close the native voice notes recorder application
+        AudioControl.closeAudioRecorder();
+    }
+
+    /**
+     * Retrieves the file properties for the captured audio recording.
+     *
+     * @param filePath
+     *            full path of the audio recording file
+     */
+    protected void processFile(String filePath) {
+        Logger.log(this.getClass().getName() + ": processing file: " + filePath);
+
+        // wait for file to finish writing and add it to captured files
+        addCaptureFile(getMediaFile(filePath));
+    }
+
+    /**
+     * Waits for file to be fully written to the file system before retrieving
+     * its file properties.
+     *
+     * @param filePath
+     *            Full path of the image file
+     * @throws IOException
+     */
+    private File getMediaFile(String filePath) {
+        File file = new File(FileUtils.stripSeparator(filePath));
+
+        // time begin waiting for file write
+        long start = (new Date()).getTime();
+
+        // wait for the file to be fully written, then grab its properties
+        FileConnection fconn = null;
+        try {
+            fconn = (FileConnection) Connector.open(filePath, Connector.READ);
+            if (fconn.exists()) {
+                // wait for file to be fully written
+                long fileSize = fconn.fileSize();
+                long size = 0;
+                Thread thisThread = Thread.currentThread();
+                while (myThread == thisThread) {
+                    try {
+                        Thread.sleep(100);
+                    }
+                    catch (InterruptedException e) {
+                        break;
+                    }
+                    size = fconn.fileSize();
+                    if (fileSize != 0 && size == fileSize) {
+                        break;
+                    }
+                    fileSize = size;
+                }
+                Logger.log(this.getClass().getName() + ": " + filePath + " size="
+                        + Long.toString(fileSize) + " bytes");
+
+                // retrieve file properties
+                file.setLastModifiedDate(fconn.lastModified());
+                file.setName(FileUtils.stripSeparator(fconn.getName()));
+                file.setSize(fileSize);
+                file.setType(MIMETypeAssociations.getMIMEType(filePath));
+            }
+        }
+        catch (IOException e) {
+            Logger.log(this.getClass().getName() + ": " + e);
+        }
+        finally {
+            try {
+                if (fconn != null) fconn.close();
+            } catch (IOException ignored) {}
+        }
+
+        // log time it took to write the file
+        long end = (new Date()).getTime();
+        Logger.log(this.getClass().getName() + ": wait time="
+                + Long.toString(end - start) + " ms");
+
+        return file;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioControl.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioControl.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioControl.java
new file mode 100644
index 0000000..45e9f9c
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/AudioControl.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.apache.cordova.util.ApplicationUtils;
+import org.apache.cordova.util.Logger;
+
+import net.rim.device.api.system.ApplicationDescriptor;
+import net.rim.device.api.system.ApplicationManager;
+import net.rim.device.api.system.ApplicationManagerException;
+import net.rim.device.api.system.CodeModuleManager;
+
+public class AudioControl {
+    /**
+     * Determines if the native voice notes recorder application is installed
+     * on the device.
+     *
+     * @return true if native voice notes recorder application is installed
+     */
+    public static boolean hasAudioRecorderApplication() {
+        return ApplicationUtils.isModuleInstalled("net_rim_bb_voicenotesrecorder");
+    }
+
+    /**
+     * Determines if the native voice notes recorder application is running in
+     * the foreground.
+     *
+     * @return true if native voice notes recorder application is running in
+     *         foreground
+     */
+    public static boolean isAudioRecorderActive() {
+        return ApplicationUtils.isApplicationInForeground("net_rim_bb_voicenotesrecorder");
+    }
+
+    /**
+     * Launches the native audio recorder application.
+     */
+    public static void launchAudioRecorder() {
+        int handle = CodeModuleManager.getModuleHandle("net_rim_bb_voicenotesrecorder");
+        ApplicationDescriptor ad = CodeModuleManager.getApplicationDescriptors(handle)[0];
+        ApplicationDescriptor ad2 = new ApplicationDescriptor(ad, null);
+        try {
+            ApplicationManager.getApplicationManager().runApplication(ad2, true);
+        }
+        catch (ApplicationManagerException e) {
+            Logger.log(AudioControl.class.getName() + ": unable to launch net_rim_bb_voicenotesrecorder");
+        }
+    }
+
+    /**
+     * Closes the native audio recorder application.
+     */
+    public static void closeAudioRecorder() {
+        if (!isAudioRecorderActive()) {
+            return;
+        }
+        ApplicationUtils.injectEscKeyPress(1);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CameraControl.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CameraControl.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CameraControl.java
new file mode 100644
index 0000000..2ed9206
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CameraControl.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.apache.cordova.util.ApplicationUtils;
+
+import net.rim.blackberry.api.invoke.CameraArguments;
+import net.rim.blackberry.api.invoke.Invoke;
+import net.rim.device.api.ui.UiApplication;
+
+public class CameraControl {
+    /**
+     * Determines if the native camera application is running in the foreground.
+     *
+     * @return true if native camera application is running in foreground
+     */
+    public static boolean isCameraActive() {
+        return ApplicationUtils.isApplicationInForeground("net_rim_bb_camera");
+    }
+
+    /**
+     * Determines if the native video recorder application is running in the
+     * foreground.
+     *
+     * @return true if native video recorder application is running in
+     *         foreground
+     */
+    public static boolean isVideoRecorderActive() {
+        return ApplicationUtils.isApplicationInForeground("net_rim_bb_videorecorder");
+    }
+
+    /**
+     * Launches the native camera application.
+     */
+    public static void launchCamera() {
+        synchronized(UiApplication.getEventLock()) {
+            Invoke.invokeApplication(Invoke.APP_TYPE_CAMERA,
+                    new CameraArguments());
+        }
+    }
+
+    /**
+     * Launches the native video recorder application.
+     */
+    public static void launchVideoRecorder() {
+        synchronized(UiApplication.getEventLock()) {
+            Invoke.invokeApplication(Invoke.APP_TYPE_CAMERA,
+                    new CameraArguments(CameraArguments.ARG_VIDEO_RECORDER));
+        }
+    }
+
+    /**
+     * Closes the native camera application.
+     */
+    public static void closeCamera() {
+        if (!isCameraActive()) {
+            return;
+        }
+        ApplicationUtils.injectEscKeyPress(2);
+    }
+
+    /**
+     * Closes the native video recorder application.
+     */
+    public static void closeVideoRecorder() {
+        if (!isVideoRecorderActive()) {
+            return;
+        }
+        ApplicationUtils.injectEscKeyPress(2);
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureControl.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureControl.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureControl.java
new file mode 100644
index 0000000..e37dd56
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureControl.java
@@ -0,0 +1,169 @@
+/*
+ * 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.Vector;
+
+public class CaptureControl {
+
+    /**
+     * Pending capture operations.
+     */
+    private Vector pendingOperations = new Vector();
+
+    /**
+     * Singleton.
+     */
+    private CaptureControl() {}
+
+    /**
+     * Holds the singleton for lazy instantiation.
+     */
+    private static class CaptureControlHolder {
+        static final CaptureControl INSTANCE = new CaptureControl();
+    }
+
+    /**
+     * Retrieves a CaptureControl instance.
+     * @return CaptureControl instance.
+     */
+    public static final CaptureControl getCaptureControl() {
+        return CaptureControlHolder.INSTANCE;
+    }
+
+    /**
+     * Add capture operation so we can stop it manually.
+     */
+    public void addCaptureOperation(CaptureOperation operation) {
+        if (operation == null) {
+            return;
+        }
+
+        synchronized (pendingOperations) {
+            pendingOperations.addElement(operation);
+        }
+    }
+
+    /**
+     * Remove capture operation.
+     */
+    public void removeCaptureOperation(CaptureOperation operation) {
+        if (operation == null) {
+            return;
+        }
+
+        synchronized (pendingOperations) {
+            pendingOperations.removeElement(operation);
+        }
+    }
+
+    /**
+     * Starts an image capture operation, during which a user can take multiple
+     * photos. The capture operation runs in the background.
+     *
+     * @param limit
+     *            the maximum number of images to capture during the operation
+     * @param callbackId
+     *            the callback to be invoked with capture file properties
+     */
+    public void startImageCaptureOperation(long limit, String callbackId) {
+        // setup a queue to receive image file paths
+        MediaQueue queue = new MediaQueue();
+
+        // start a capture operation on a background thread
+        CaptureOperation operation = new ImageCaptureOperation(limit,
+                callbackId, queue);
+
+        // track the operation so we can stop or cancel it later
+        addCaptureOperation(operation);
+    }
+
+    /**
+     * Starts a video capture operation, during which a user can record multiple
+     * recordings.  The capture operation runs in the background.
+     *
+     * @param limit
+     *            the maximum number of images to capture during the operation
+     * @param callbackId
+     *            the callback to be invoked with capture file properties
+     */
+    public void startVideoCaptureOperation(long limit, String callbackId) {
+        // setup a queue to receive video recording file paths
+        MediaQueue queue = new MediaQueue();
+
+        // start a capture operation on a background thread
+        CaptureOperation operation = new VideoCaptureOperation(limit,
+                callbackId, queue);
+
+        // track the operation so we can stop or cancel it later
+        addCaptureOperation(operation);
+    }
+
+    /**
+     * Starts an audio capture operation using the native voice notes recorder
+     * application.
+     *
+     * @param limit
+     *            the maximum number of audio clips to capture during the
+     *            operation
+     * @param duration
+     *            the maximum duration of each captured clip
+     * @param callbackId
+     *            the callback to be invoked with the capture results
+     */
+    public void startAudioCaptureOperation(long limit, double duration, String callbackId) {
+        // setup a queue to receive recording file paths
+        MediaQueue queue = new MediaQueue();
+
+        // start a capture operation on a background thread
+        CaptureOperation operation = new AudioCaptureOperation(limit, duration,
+                callbackId, queue);
+
+        // track the operation so we can stop or cancel it later
+        addCaptureOperation(operation);
+    }
+
+    /**
+     * Stops all pending capture operations. If the <code>cancel</code>
+     * parameter is <code>true</code>, no results will be sent via the callback
+     * mechanism and any captured files will be removed from the file system.
+     *
+     * @param cancel
+     *            true if operations should be canceled
+     */
+    public void stopPendingOperations(boolean cancel) {
+        // There are two scenarios where the capture operation would be stopped
+        // manually:
+        // 1- The user stops the capture application, and this application
+        //    returns to the foreground.
+        // 2- It is canceled programmatically.  No results should be sent.
+        synchronized (pendingOperations) {
+            for (Enumeration e = pendingOperations.elements(); e.hasMoreElements(); ) {
+                CaptureOperation operation = (CaptureOperation) e.nextElement();
+                if (cancel) {
+                    operation.cancel();
+                }
+                else {
+                    operation.stop();
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureMode.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureMode.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureMode.java
new file mode 100644
index 0000000..7c71f96
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureMode.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+public class CaptureMode {
+
+    private String mimeType = null;
+    private long height = 0;
+    private long width = 0;
+
+    public CaptureMode() {
+    }
+
+    public CaptureMode(String type) {
+        this.mimeType = type;
+    }
+
+    public CaptureMode(String type, long width, long height) {
+        this.mimeType = type;
+        this.height = height;
+        this.width = width;
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+
+    public long getHeight() {
+        return height;
+    }
+
+    public long getWidth() {
+        return width;
+    }
+
+    public JSONObject toJSONObject() {
+        JSONObject o = new JSONObject();
+        try {
+            o.put("type", getMimeType());
+            o.put("height", getHeight());
+            o.put("width", getWidth());
+        }
+        catch (JSONException ignored) {
+        }
+        return o;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof CaptureMode)) {
+            return false;
+        }
+        CaptureMode cm = (CaptureMode)o;
+        return ((mimeType == null ? cm.mimeType == null :
+            mimeType.equals(cm.mimeType))
+            && (width == cm.width)
+            && (height == cm.height));
+    }
+
+    public int hashCode() {
+        int hash = (mimeType != null ? mimeType.hashCode() : 19);
+        hash = 37*hash + (int)width;
+        hash = 37*hash + (int)height;
+        return hash;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureOperation.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureOperation.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureOperation.java
new file mode 100644
index 0000000..dc85bd8
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/CaptureOperation.java
@@ -0,0 +1,202 @@
+/*
+ * 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 java.util.Enumeration;
+import java.util.Vector;
+
+import org.apache.cordova.file.File;
+import org.apache.cordova.util.FileUtils;
+import org.apache.cordova.util.Logger;
+
+public abstract class CaptureOperation implements Runnable {
+    // max number of media files to capture
+    protected long limit = 1;
+
+    // for sending results
+    protected String callbackId = null;
+
+    // list of captured media files
+    protected final Vector captureFiles = new Vector();
+
+    // media file queue
+    protected MediaQueue mediaQueue = null;
+
+    // used to interrupt thread
+    protected volatile Thread myThread;
+
+    // to determine if operation has been canceled
+    protected boolean canceled = false;
+
+    /**
+     * Creates and starts a capture operation on a new thread.
+     *
+     * @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 CaptureOperation(long limit, String callbackId, MediaQueue queue) {
+        if (limit > 1) {
+            this.limit = limit;
+        }
+
+        this.callbackId = callbackId;
+        this.mediaQueue = queue;
+        this.myThread = new Thread(this);
+    }
+
+    /**
+     * Waits for media file to be captured.
+     */
+    public void run() {
+        if (myThread == null) {
+            return; // stopped before started
+        }
+
+        Logger.log(this.getClass().getName() + ": " + callbackId + " started");
+
+        // tasks to be run before entering main loop
+        setup();
+
+        // capture until interrupted or we've reached capture limit
+        Thread thisThread = Thread.currentThread();
+        String filePath = null;
+        while (myThread == thisThread && captureFiles.size() < limit) {
+            try {
+                // consume file added to media capture queue
+                filePath = mediaQueue.remove();
+            }
+            catch (InterruptedException e) {
+                Logger.log(this.getClass().getName() + ": " + callbackId + " interrupted");
+                // and we're done
+                break;
+            }
+            processFile(filePath);
+        }
+
+        // perform cleanup tasks
+        teardown();
+
+        // process captured results
+        processResults();
+
+        // unregister the operation from the controller
+        CaptureControl.getCaptureControl().removeCaptureOperation(this);
+
+        Logger.log(this.getClass().getName() + ": " + callbackId + " finished");
+    }
+
+    /**
+     * Starts this capture operation on a new thread.
+     */
+    protected void start() {
+        if (myThread == null) {
+            return; // stopped before started
+        }
+        myThread.start();
+    }
+
+    /**
+     * Stops the operation.
+     */
+    public void stop() {
+        // interrupt capture thread
+        Thread tmpThread = myThread;
+        myThread = null;
+        if (tmpThread != null && tmpThread.isAlive()) {
+            tmpThread.interrupt();
+        }
+    }
+
+    /**
+     * Cancels the operation.
+     */
+    public void cancel() {
+        canceled = true;
+        stop();
+    }
+
+    /**
+     * Processes the results of the capture operation.
+     */
+    protected void processResults() {
+        // process results
+        if (!canceled) {
+            // invoke appropriate callback
+            if (captureFiles.size() > 0) {
+                // send capture files
+                MediaCapture.captureSuccess(captureFiles, callbackId);
+            }
+            else {
+                // error
+                MediaCapture.captureError(callbackId);
+            }
+        }
+        else {
+            removeCaptureFiles();
+        }
+    }
+
+    /**
+     * Adds a media file to list of collected media files for this operation.
+     *
+     * @param file
+     *            object containing media file properties
+     */
+    protected void addCaptureFile(File file) {
+        captureFiles.addElement(file);
+    }
+
+    /**
+     * Removes captured files from the file system.
+     */
+    protected void removeCaptureFiles() {
+        for (Enumeration e = captureFiles.elements(); e.hasMoreElements();) {
+            File file = (File) e.nextElement();
+            try {
+                FileUtils.delete(file.getFullPath());
+            }
+            catch (IOException ignored) {
+            }
+        }
+    }
+
+    /**
+     * Override this method to perform tasks before the operation starts.
+     */
+    protected void setup() {
+    }
+
+    /**
+     * Override this method to perform tasks after the operation has
+     * stopped.
+     */
+    protected void teardown() {
+    }
+
+    /**
+     * Subclasses must implement this method to process a captured media file.
+     * @param filePath the full path of the media file
+     */
+    protected abstract void processFile(final String filePath);
+}

http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureListener.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureListener.java
new file mode 100644
index 0000000..4906ee8
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureListener.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.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 image files that are added to file system.
+ * <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 ImageCaptureListener implements FileSystemJournalListener {
+
+    /**
+     * Used to track file system changes.
+     */
+    private long lastUSN = 0;
+
+    /**
+     * Collection of media files.
+     */
+    private MediaQueue queue = null;
+
+    /**
+     * Constructor.
+     */
+    ImageCaptureListener(MediaQueue queue) {
+        this.queue = queue;
+    }
+
+    /**
+     * Listens for file system changes.  When a JPEG file is added, we process
+     * it and send it back.
+     */
+    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;
+            }
+
+            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED)
+            {
+                String path = entry.getPath();
+                if (path != null && path.indexOf(".jpg") != -1)
+                {
+                    // add file path to the capture queue
+                    queue.add("file://" + path);
+                    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/ImageCaptureOperation.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureOperation.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureOperation.java
new file mode 100644
index 0000000..a831dc2
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/ImageCaptureOperation.java
@@ -0,0 +1,161 @@
+/*
+ * 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 java.util.Date;
+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 ImageCaptureOperation extends CaptureOperation {
+    // content type
+    public static String CONTENT_TYPE = "image/";
+
+    // file system listener
+    private ImageCaptureListener 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 ImageCaptureOperation(long limit, String callbackId, MediaQueue queue) {
+        super(limit, callbackId, queue);
+
+        // listener to capture image files added to file system
+        this.listener = new ImageCaptureListener(queue);
+
+        start();
+    }
+
+    /**
+     * Registers file system listener and launches native camera application.
+     */
+    protected void setup() {
+        // register listener for files being written
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().addFileSystemJournalListener(listener);
+        }
+
+        // launch the native camera application
+        CameraControl.launchCamera();
+    }
+
+    /**
+     * Unregisters file system listener and closes native camera application.
+     */
+    protected void teardown() {
+        // remove file system listener
+        synchronized(UiApplication.getEventLock()) {
+            UiApplication.getUiApplication().removeFileSystemJournalListener(listener);
+        }
+
+        // close the native camera application
+        CameraControl.closeCamera();
+    }
+
+    /**
+     * Waits for image file to be written to file system and retrieves its file
+     * properties.
+     *
+     * @param filePath
+     *            the full path of the media file
+     */
+    protected void processFile(final String filePath) {
+        Logger.log(this.getClass().getName() + ": processing file: " + filePath);
+
+        // wait for file to finish writing and add it to captured files
+        addCaptureFile(getMediaFile(filePath));
+    }
+
+    /**
+     * Waits for file to be fully written to the file system before retrieving
+     * its file properties.
+     *
+     * @param filePath
+     *            Full path of the image file
+     * @throws IOException
+     */
+    private File getMediaFile(String filePath) {
+        File file = new File(FileUtils.stripSeparator(filePath));
+
+        // time begin waiting for file write
+        long start = (new Date()).getTime();
+
+        // wait for the file to be fully written, then grab its properties
+        FileConnection fconn = null;
+        try {
+            fconn = (FileConnection) Connector.open(filePath, Connector.READ);
+            if (fconn.exists()) {
+                // wait for file to be fully written
+                long fileSize = fconn.fileSize();
+                long size = 0;
+                Thread thisThread = Thread.currentThread();
+                while (myThread == thisThread) {
+                    try {
+                        Thread.sleep(100);
+                    }
+                    catch (InterruptedException e) {
+                        break;
+                    }
+                    size = fconn.fileSize();
+                    if (size == fileSize) {
+                        break;
+                    }
+                    fileSize = size;
+                }
+                Logger.log(this.getClass().getName() + ": " + filePath + " size="
+                        + Long.toString(fileSize) + " bytes");
+
+                // retrieve file properties
+                file.setLastModifiedDate(fconn.lastModified());
+                file.setName(FileUtils.stripSeparator(fconn.getName()));
+                file.setSize(fileSize);
+                file.setType(MIMETypeAssociations.getMIMEType(filePath));
+            }
+        }
+        catch (IOException e) {
+            Logger.log(this.getClass().getName() + ": " + e);
+        }
+        finally {
+            try {
+                if (fconn != null) fconn.close();
+            } catch (IOException ignored) {}
+        }
+
+        // log time it took to write the file
+        long end = (new Date()).getTime();
+        Logger.log(this.getClass().getName() + ": wait time="
+                + Long.toString(end - start) + " ms");
+
+        return file;
+    }
+}