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