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;
+ }
+}