Return-Path: X-Original-To: apmail-cordova-commits-archive@www.apache.org Delivered-To: apmail-cordova-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id AF9A9EF46 for ; Tue, 22 Jan 2013 01:58:00 +0000 (UTC) Received: (qmail 97279 invoked by uid 500); 22 Jan 2013 01:58:00 -0000 Delivered-To: apmail-cordova-commits-archive@cordova.apache.org Received: (qmail 97225 invoked by uid 500); 22 Jan 2013 01:58:00 -0000 Mailing-List: contact commits-help@cordova.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: callback-dev@cordova.apache.org Delivered-To: mailing list commits@cordova.apache.org Received: (qmail 96267 invoked by uid 99); 22 Jan 2013 01:57:59 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 22 Jan 2013 01:57:59 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id DA6BA822AD6; Tue, 22 Jan 2013 01:57:58 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: filmaj@apache.org To: commits@cordova.apache.org X-Mailer: ASF-Git Admin Mailer Subject: [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. Message-Id: <20130122015758.DA6BA822AD6@tyr.zones.apache.org> Date: Tue, 22 Jan 2013 01:57:58 +0000 (UTC) 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. + *

+ * 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 FileSystemJournalEntry.FILE_ADDED event, capturing when + * the new file is written. + *

+ * 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 cancel + * parameter is true, 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. + *

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