/* * PhoneGap is available under *either* the terms of the modified BSD license *or* the * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text. * * Copyright (c) 2005-2010, Nitobi Software Inc. * Copyright (c) 2010, IBM Corporation */ package com.phonegap.camera; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; import javax.microedition.io.Connector; import javax.microedition.io.file.FileConnection; 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.Characters; import net.rim.device.api.system.ControlledAccessException; import net.rim.device.api.system.EventInjector; import net.rim.device.api.ui.UiApplication; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; import com.phonegap.json4j.JSONArray; import com.phonegap.util.Logger; /** * 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"; /** * Destination type determines what result will be returned. */ public static final int DATA_URL = 0; public static final int FILE_URI = 1; /** * 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)) { /** * args JSONArray formatted as [ cameraArgs ], where cameraArgs: * [ 80, // quality (ignored) * Camera.DestinationType.DATA_URL, // destinationType * Camera.PictureSourceType.PHOTOLIBRARY // sourceType (ignored)] */ // determine the desired destination type: encoded image or file URI int destinationType = DATA_URL; if (args != null && args.length() > 1 && !args.isNull(1)) { Integer destType = (Integer)args.opt(1); if (destType.intValue()== FILE_URI) destinationType = FILE_URI; } // launch native camera application launchCamera(new PhotoListener(destinationType, 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.INVALIDACTION, "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 destinationType Specifies the format of the result * @param callbackId The id of the callback to receive the result */ public static void processImage(String filePath, int destinationType, String callbackId) { Logger.log(Camera.class.getName() + ": processing image " + filePath); 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); if (destinationType == Camera.FILE_URI) { // return just the photo URI result = new PluginResult(PluginResult.Status.OK, filePath); } else { String encodedImage = encodeImage(filePath); // we have to check the size to avoid memory errors in the browser if (encodedImage.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, encodedImage); } } } catch (Exception e) { result = new PluginResult(PluginResult.Status.IOEXCEPTION, 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; } /** * 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); } } }