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
[25/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/capture/MediaCapture.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java
new file mode 100644
index 0000000..76b0eac
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaCapture.java
@@ -0,0 +1,502 @@
+/*
+ * 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.Hashtable;
+import java.util.Vector;
+
+import javax.microedition.media.Manager;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.file.File;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+import org.apache.cordova.util.Logger;
+import org.apache.cordova.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";
+
+ private static final String LOG_TAG = "MediaCapture: ";
+
+ /**
+ * 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 String ACTION_GET_SUPPORTED_MODES = "captureModes";
+ protected static final String ACTION_CAPTURE_AUDIO = "captureAudio";
+ protected static final String ACTION_CAPTURE_IMAGE = "captureImage";
+ protected static final String ACTION_CAPTURE_VIDEO = "captureVideo";
+ protected static final String ACTION_CANCEL_CAPTURES = "stopCaptures";
+
+ /**
+ * 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;
+
+ if (ACTION_GET_SUPPORTED_MODES.equals(action)) {
+ result = getCaptureModes();
+ } else if (ACTION_CAPTURE_AUDIO.equals(action)) {
+ result = captureAudio(args, callbackId);
+ } else if (ACTION_CAPTURE_IMAGE.equals(action)) {
+ result = captureImage(args, callbackId);
+ } else if (ACTION_CAPTURE_VIDEO.equals(action)) {
+ result = captureVideo(args, callbackId);
+ } else if (ACTION_CANCEL_CAPTURES.equals(action)) {
+ CaptureControl.getCaptureControl().stopPendingOperations(true);
+ result = new PluginResult(PluginResult.Status.OK);
+ } else {
+ result = new PluginResult(PluginResult.Status.INVALID_ACTION,
+ "MediaCapture: invalid action " + action);
+ }
+
+ return result;
+ }
+
+ /**
+ * 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()));
+ }
+
+ /**
+ * Return the supported capture modes for audio, image and video.
+ * @return supported capture modes.
+ */
+ private PluginResult getCaptureModes() {
+ JSONArray audioModes = new JSONArray();
+ JSONArray imageModes = new JSONArray();
+ boolean audioSupported = isAudioCaptureSupported();
+
+ // 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=");
+ CaptureMode mode = null;
+ Vector list = new Vector();
+
+ // get all supported capture content types for audio and image
+ String[] contentTypes = getCaptureContentTypes();
+ for (int i = 0; i < contentTypes.length; i++) {
+ if (audioSupported
+ && contentTypes[i]
+ .startsWith(AudioCaptureOperation.CONTENT_TYPE)) {
+ audioModes.add(new CaptureMode(contentTypes[i]).toJSONObject());
+ } else 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);
+ imageModes.add(mode.toJSONObject());
+ }
+ }
+ }
+ }
+ }
+
+ JSONObject captureModes = new JSONObject();
+ try {
+ captureModes.put("supportedAudioModes", audioModes.toString());
+ captureModes.put("supportedImageModes", imageModes.toString());
+ captureModes.put("supportedVideoModes", getVideoCaptureModes().toString());
+ } catch (JSONException e) {
+ Logger.error("JSONException: " + e.getMessage());
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ "Failed to build supported capture modes.");
+ }
+
+ return new PluginResult(PluginResult.Status.OK, captureModes);
+ }
+
+ /**
+ * Retrieves supported video capture modes (content type, width and height).
+ * @return supported video capture modes
+ */
+ protected JSONArray getVideoCaptureModes() {
+ JSONArray videoModes = new JSONArray();
+
+ 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 videoModes;
+ }
+
+ /**
+ * 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
+ 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);
+ videoModes.add(mode.toJSONObject());
+ }
+ }
+ }
+
+ return videoModes;
+ }
+
+ /**
+ * 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()) {
+ result = errorResult(CAPTURE_NOT_SUPPORTED,
+ "Audio recorder application is not installed.");
+ } else if (AudioControl.isAudioRecorderActive()) {
+ result = errorResult(CAPTURE_APPLICATION_BUSY,
+ "Audio recorder application is busy.");
+ }
+ else {
+ // optional parameters
+ long limit = 1;
+ double duration = 0.0f;
+
+ try {
+ JSONObject options = args.getJSONObject(0);
+ if (options != null) {
+ limit = options.optLong("limit", 1);
+ duration = options.optDouble("duration", 0.0f);
+ }
+ } catch (JSONException e) {
+ // Eat it and use default value of 1.
+ Logger.log(this.getClass().getName()
+ + ": Invalid captureAudio options format. " + e.getMessage());
+ }
+
+ // 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()) {
+ result = errorResult(CAPTURE_APPLICATION_BUSY,
+ "Camera application is busy.");
+ }
+ else {
+ // optional parameters
+ long limit = 1;
+
+ try {
+ JSONObject options = args.getJSONObject(0);
+ if (options != null) {
+ limit = options.optLong("limit", 1);
+ }
+ } catch (JSONException e) {
+ // Eat it and use default value of 1.
+ Logger.log(this.getClass().getName()
+ + ": Invalid captureImage options format. " + e.getMessage());
+ }
+
+ // 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()) {
+ result = errorResult(CAPTURE_NOT_SUPPORTED,
+ "Video capture is not supported.");
+ } else if (CameraControl.isVideoRecorderActive()) {
+ result = errorResult(CAPTURE_APPLICATION_BUSY,
+ "Video recorder application is busy.");
+ }
+ else {
+ // optional parameters
+ long limit = 1;
+
+ try {
+ JSONObject options = args.getJSONObject(0);
+ if (options != null) {
+ limit = options.optLong("limit", 1);
+ }
+ } catch (JSONException e) {
+ // Eat it and use default value of 1.
+ Logger.log(this.getClass().getName()
+ + ": Invalid captureVideo options format. " + e.getMessage());
+ }
+
+ // 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);
+ success(result, callbackId);
+ }
+
+ /**
+ * Sends error back to JavaScript.
+ *
+ * @param callbackId
+ * the callback to receive the error
+ */
+ public static void captureError(String callbackId) {
+ error(errorResult(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);
+ }
+
+ private static PluginResult errorResult(int code, String message) {
+ Logger.log(LOG_TAG + message);
+
+ JSONObject obj = new JSONObject();
+ try {
+ obj.put("code", code);
+ obj.put("message", message);
+ } catch (JSONException e) {
+ // This will never happen
+ }
+
+ return new PluginResult(PluginResult.Status.ERROR, obj);
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
new file mode 100644
index 0000000..56ffff5
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/MediaQueue.java
@@ -0,0 +1,44 @@
+/*
+ * 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.Vector;
+
+/**
+ * Acts as a container for captured media files. The media applications will
+ * add to the queue when a media file is captured.
+ */
+class MediaQueue {
+ private Vector queue = new Vector();
+
+ synchronized void add(final String filePath) {
+ queue.addElement(filePath);
+ notifyAll();
+ }
+
+ synchronized String remove() throws InterruptedException {
+ while (queue.size() == 0) {
+ wait();
+ }
+ String filePath = (String) queue.firstElement();
+ queue.removeElement(filePath);
+ notifyAll();
+ return filePath;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.java
new file mode 100644
index 0000000..361b7ee
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureListener.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.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 video recording files that are added to file system.
+ * <p>
+ * Video recordings are added to the file system in a multi-step process. The
+ * video recorder application records the video on a background thread. While
+ * the recording is in progress, it is added to the file system with a '.lock'
+ * extension. When the user stops the recording, the file is renamed to the
+ * video recorder extension (e.g. .3GP). Therefore, we listen for the
+ * <code>FileSystemJournalEntry.FILE_RENAMED</code> event, capturing when the
+ * new path name ends in the video recording file extension.
+ * <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 VideoCaptureListener implements FileSystemJournalListener {
+
+ /**
+ * Used to track file system changes.
+ */
+ private long lastUSN = 0;
+
+ /**
+ * Queue to send media files to for processing.
+ */
+ private MediaQueue queue = null;
+
+ /**
+ * Newly added video recording.
+ */
+ private String newFilePath = null;
+
+ /**
+ * Constructor.
+ */
+ VideoCaptureListener(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;
+ }
+
+ String path = entry.getPath();
+ if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED
+ && newFilePath == null) {
+ // a new file has been added to the file system
+ // if it has a video recording extension, store it until
+ // it is renamed, indicating it has finished being written to
+ int index = path.indexOf(".3GP");
+ if (index == -1) {
+ index = path.indexOf(".MP4");
+ }
+ if (index != -1) {
+ newFilePath = path.substring(0, index + 4);
+ }
+ }
+ else if (entry.getEvent() == FileSystemJournalEntry.FILE_RENAMED) {
+ if (path != null && path.equals(newFilePath))
+ {
+ // add file path to the capture queue
+ queue.add("file://" + path);
+
+ // get ready for next file
+ newFilePath = null;
+ 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/VideoCaptureOperation.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java
new file mode 100644
index 0000000..589bc68
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/capture/VideoCaptureOperation.java
@@ -0,0 +1,124 @@
+/*
+ * 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 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 VideoCaptureOperation extends CaptureOperation {
+
+ // content type
+ public static String CONTENT_TYPE = "video/";
+
+ // file system listener
+ private VideoCaptureListener 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 VideoCaptureOperation(long limit, String callbackId, MediaQueue queue) {
+ super(limit, callbackId, queue);
+
+ // listener to capture image files added to file system
+ this.listener = new VideoCaptureListener(queue);
+
+ start();
+ }
+
+ /**
+ * Registers file system listener and launches native video recorder
+ * application.
+ */
+ protected void setup() {
+ // register listener for files being written
+ synchronized(UiApplication.getEventLock()) {
+ UiApplication.getUiApplication().addFileSystemJournalListener(listener);
+ }
+
+ // launch the native video recorder application
+ CameraControl.launchVideoRecorder();
+ }
+
+ /**
+ * Unregisters file system listener and closes native video recorder
+ * application.
+ */
+ protected void teardown() {
+ // remove file system listener
+ synchronized(UiApplication.getEventLock()) {
+ UiApplication.getUiApplication().removeFileSystemJournalListener(listener);
+ }
+
+ // close the native video recorder application
+ CameraControl.closeVideoRecorder();
+ }
+
+ /**
+ * Retrieves the file properties for the captured video recording.
+ *
+ * @param filePath
+ * full path of the video recording file
+ */
+ protected void processFile(String filePath) {
+ Logger.log(this.getClass().getName() + ": processing file: " + filePath);
+
+ File file = new File(FileUtils.stripSeparator(filePath));
+
+ // grab file properties
+ FileConnection fconn = null;
+ try {
+ fconn = (FileConnection) Connector.open(filePath, Connector.READ);
+ if (fconn.exists()) {
+ long size = fconn.fileSize();
+ Logger.log(this.getClass().getName() + ": " + filePath + " size="
+ + Long.toString(size) + " bytes");
+ file.setLastModifiedDate(fconn.lastModified());
+ file.setName(FileUtils.stripSeparator(fconn.getName()));
+ file.setSize(size);
+ file.setType(MIMETypeAssociations.getMIMEType(filePath));
+ }
+ }
+ catch (IOException e) {
+ Logger.log(this.getClass().getName() + ": " + e);
+ }
+ finally {
+ try {
+ if (fconn != null) fconn.close();
+ } catch (IOException ignored) {}
+ }
+
+ addCaptureFile(file);
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
new file mode 100644
index 0000000..e11f924
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/device/Device.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ *
+ * Copyright (c) 2011, Research In Motion Limited.
+ */
+package org.apache.cordova.device;
+
+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.json4j.JSONObject;
+
+import net.rim.device.api.system.DeviceInfo;
+
+/**
+ * Provides device information, including:
+ *
+ * - Device platform version (e.g. 2.13.0.95). Not to be confused with BlackBerry OS version.
+ * - Unique device identifier (UUID).
+ * - Cordova software version.
+ */
+public final class Device extends Plugin {
+
+ public static final String FIELD_PLATFORM = "platform";
+ public static final String FIELD_UUID = "uuid";
+ public static final String FIELD_CORDOVA = "cordova";
+ public static final String FIELD_MODEL = "model";
+ public static final String FIELD_NAME = "name";
+ public static final String FIELD_VERSION = "version";
+
+ public static final String ACTION_GET_DEVICE_INFO = "getDeviceInfo";
+
+ public PluginResult execute(String action, JSONArray args, String callbackId) {
+ PluginResult result = new PluginResult(PluginResult.Status.INVALID_ACTION, "Device: Invalid action:" + action);
+
+ if(action.equals(ACTION_GET_DEVICE_INFO)){
+ try {
+ JSONObject device = new JSONObject();
+ device.put( FIELD_PLATFORM, "BlackBerry");
+ device.put( FIELD_UUID, new Integer( DeviceInfo.getDeviceId()) );
+ device.put( FIELD_CORDOVA, "2.4.0rc1" );
+ device.put( FIELD_MODEL, new String(DeviceInfo.getDeviceName()) );
+ device.put( FIELD_NAME, new String(DeviceInfo.getDeviceName()) );
+ device.put( FIELD_VERSION, new String(DeviceInfo.getSoftwareVersion()) );
+ result = new PluginResult(PluginResult.Status.OK, device);
+ } catch (JSONException e) {
+ result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
+ }
+ }
+
+ return result;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
new file mode 100644
index 0000000..66fb59b
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/Entry.java
@@ -0,0 +1,66 @@
+/*
+ * 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.file;
+
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+public class Entry {
+
+ private boolean isDirectory = false;
+ private String name = null;
+ private String fullPath = null;
+
+ public boolean isDirectory() {
+ return isDirectory;
+ }
+
+ public void setDirectory(boolean isDirectory) {
+ this.isDirectory = isDirectory;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getFullPath() {
+ return fullPath;
+ }
+
+ public void setFullPath(String fullPath) {
+ this.fullPath = fullPath;
+ }
+
+ public JSONObject toJSONObject() {
+ JSONObject o = new JSONObject();
+ try {
+ o.put("isDirectory", isDirectory);
+ o.put("isFile", !isDirectory);
+ o.put("name", name);
+ o.put("fullPath", fullPath);
+ }
+ catch (JSONException ignored) {
+ }
+ return o;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.java
new file mode 100644
index 0000000..3d04041
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/File.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.file;
+
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+
+public class File {
+ private String name = null;
+ private String fullPath = null;
+ private String type = null;
+ private long lastModifiedDate;
+ private long size = 0;
+
+ public File(String filePath) {
+ this.fullPath = filePath;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public long getLastModifiedDate() {
+ return lastModifiedDate;
+ }
+
+ public void setLastModifiedDate(long lastModifiedDate) {
+ this.lastModifiedDate = lastModifiedDate;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public String getFullPath() {
+ return fullPath;
+ }
+
+ public JSONObject toJSONObject() {
+ JSONObject o = new JSONObject();
+ try {
+ o.put("fullPath", fullPath);
+ o.put("type", type);
+ o.put("name", name);
+ o.put("lastModifiedDate", lastModifiedDate);
+ o.put("size", size);
+ }
+ catch (JSONException ignored) {
+ }
+ return o;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
----------------------------------------------------------------------
diff --git a/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
new file mode 100644
index 0000000..e0c3556
--- /dev/null
+++ b/lib/cordova-blackberry/framework/ext/src/org/apache/cordova/file/FileManager.java
@@ -0,0 +1,1044 @@
+/*
+ * 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.file;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Enumeration;
+
+import javax.microedition.io.Connector;
+import javax.microedition.io.file.FileConnection;
+import javax.microedition.io.file.FileSystemRegistry;
+
+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.json4j.JSONObject;
+import org.apache.cordova.util.FileUtils;
+import org.apache.cordova.util.Logger;
+
+import net.rim.device.api.io.Base64OutputStream;
+import net.rim.device.api.io.FileNotFoundException;
+import net.rim.device.api.io.MIMETypeAssociations;
+import net.rim.device.api.system.Application;
+
+public class FileManager extends Plugin {
+
+ /**
+ * File related errors.
+ */
+ public static int NOT_FOUND_ERR = 1;
+ public static int SECURITY_ERR = 2;
+ public static int ABORT_ERR = 3;
+ public static int NOT_READABLE_ERR = 4;
+ public static int ENCODING_ERR = 5;
+ public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+ public static int INVALID_STATE_ERR = 7;
+ public static int SYNTAX_ERR = 8;
+ public static int INVALID_MODIFICATION_ERR = 9;
+ public static int QUOTA_EXCEEDED_ERR = 10;
+ public static int TYPE_MISMATCH_ERR = 11;
+ public static int PATH_EXISTS_ERR = 12;
+
+ /**
+ * File system for storing information on a temporary basis (no guaranteed persistence).
+ */
+ public static final short FS_TEMPORARY = 0;
+
+ /**
+ * File system for storing information on a permanent basis.
+ */
+ public static final short FS_PERSISTENT = 1;
+
+ /**
+ * Possible actions.
+ */
+ protected static String ACTION_READ_AS_TEXT = "readAsText";
+ protected static String ACTION_READ_AS_DATA_URL = "readAsDataURL";
+ protected static String ACTION_WRITE = "write";
+ protected static String ACTION_TRUNCATE = "truncate";
+ protected static String ACTION_REQUEST_FILE_SYSTEM = "requestFileSystem";
+ protected static String ACTION_RESOLVE_FILE_SYSTEM_URI = "resolveLocalFileSystemURI";
+ protected static String ACTION_GET_METADATA = "getMetadata";
+ protected static String ACTION_GET_FILE_METADATA = "getFileMetadata";
+ protected static String ACTION_LIST_DIRECTORY = "readEntries";
+ protected static String ACTION_COPY_TO = "copyTo";
+ protected static String ACTION_MOVE_TO = "moveTo";
+ protected static String ACTION_IS_FILE_SYSTEM_ROOT = "isFileSystemRoot";
+
+ /**
+ * 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) {
+
+ // perform specified action
+ if (ACTION_READ_AS_TEXT.equals(action)) {
+ // get file path
+ String filePath = null;
+ try {
+ filePath = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return readAsText(filePath, args.optString(1));
+ }
+ else if (ACTION_READ_AS_DATA_URL.equals(action)) {
+ // get file path
+ String filePath = null;
+ try {
+ filePath = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return readAsDataURL(filePath);
+ }
+ else if (ACTION_WRITE.equals(action)) {
+ // file path
+ String filePath = null;
+ try {
+ filePath = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+
+ // file data
+ String data = null;
+ try {
+ data = args.getString(1);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Unable to parse file data: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+
+ // position
+ int position = 0;
+ try {
+ position = Integer.parseInt(args.optString(2));
+ }
+ catch (NumberFormatException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid position parameter: " + e);
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return writeFile(filePath, data, position);
+ }
+ else if (ACTION_TRUNCATE.equals(action)) {
+ // file path
+ String filePath = null;
+ try {
+ filePath = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+
+ // file size
+ long fileSize = 0;
+ try {
+ // retrieve new file size
+ fileSize = Long.parseLong(args.getString(1));
+ }
+ catch (Exception e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid file size parameter: " + e);
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return truncateFile(filePath, fileSize);
+ }
+ else if (ACTION_REQUEST_FILE_SYSTEM.equals(action)) {
+ int fileSystemType = -1;
+ long fileSystemSize = 0;
+ try {
+ fileSystemType = args.getInt(0);
+ fileSystemSize = (args.isNull(1) == true) ? 0 : args.getLong(1);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid file system type: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return requestFileSystem(fileSystemType, fileSystemSize);
+ }
+ else if (ACTION_RESOLVE_FILE_SYSTEM_URI.equals(action)) {
+ String uri = null;
+ try {
+ uri = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing file URI: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return resolveFileSystemURI(uri);
+ }
+ else if (ACTION_GET_METADATA.equals(action) || ACTION_GET_FILE_METADATA.equals(action)) {
+ String path = null;
+ try {
+ path = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing file URI: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return getMetadata(path, ACTION_GET_FILE_METADATA.equals(action));
+ }
+ else if (ACTION_LIST_DIRECTORY.equals(action)) {
+ String path = null;
+ try {
+ path = args.getString(0);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return listDirectory(path);
+ }
+ else if (ACTION_COPY_TO.equals(action)) {
+ String srcPath = null;
+ String parent = null;
+ String newName = null;
+ try {
+ srcPath = args.getString(0);
+ parent = args.getString(1);
+ newName = args.getString(2);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return copyTo(srcPath, parent, newName);
+ }
+ else if (ACTION_MOVE_TO.equals(action)) {
+ String srcPath = null;
+ String parent = null;
+ String newName = null;
+ try {
+ srcPath = args.getString(0);
+ parent = args.getString(1);
+ newName = args.getString(2);
+ }
+ catch (JSONException e) {
+ Logger.log(this.getClass().getName()
+ + ": Invalid or missing path: " + e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+ return moveTo(srcPath, parent, newName);
+ }
+ else if (ACTION_IS_FILE_SYSTEM_ROOT.equals(action)) {
+ return new PluginResult(PluginResult.Status.OK,
+ isFileSystemRoot(args.optString(0)));
+ }
+
+ // invalid action
+ return new PluginResult(PluginResult.Status.INVALID_ACTION,
+ "File: invalid action " + action);
+ }
+
+ /**
+ * Reads a file and encodes the contents using the specified encoding.
+ *
+ * @param filePath
+ * Full path of the file to be read
+ * @param encoding
+ * Encoding to use for the file contents
+ * @return PluginResult containing encoded file contents or error code if
+ * unable to read or encode file
+ */
+ protected static PluginResult readAsText(String filePath, String encoding) {
+ PluginResult result = null;
+ String logMsg = ": encoding file contents using " + encoding;
+
+ // read the file
+ try {
+ // return encoded file contents
+ byte[] blob = FileUtils.readFile(filePath, Connector.READ);
+ result = new PluginResult(PluginResult.Status.OK,
+ new String(blob, encoding));
+ }
+ catch (FileNotFoundException e) {
+ logMsg = e.toString();
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ catch (UnsupportedEncodingException e) {
+ logMsg = e.toString();
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ ENCODING_ERR);
+ }
+ catch (IOException e) {
+ logMsg = e.toString();
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_READABLE_ERR);
+ }
+ finally {
+ Logger.log(FileManager.class.getName() + ": " + logMsg);
+ }
+
+ return result;
+ }
+
+ /**
+ * Read file and return data as a base64 encoded data url. A data url is of
+ * the form: data:[<mediatype>][;base64],<data>
+ *
+ * @param filePath
+ * Full path of the file to be read
+ * @return PluginResult containing the encoded file contents or an error
+ * code if unable to read the file
+ */
+ protected static PluginResult readAsDataURL(String filePath) {
+ String data = null;
+ try {
+ // read file
+ byte[] blob = FileUtils.readFile(filePath, Connector.READ);
+
+ // encode file contents using BASE64 encoding
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ Base64OutputStream base64OutputStream = new Base64OutputStream(
+ byteArrayOutputStream);
+ base64OutputStream.write(blob);
+ base64OutputStream.flush();
+ base64OutputStream.close();
+ data = byteArrayOutputStream.toString();
+ }
+ catch (FileNotFoundException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_READABLE_ERR);
+ }
+
+ // put result in proper form
+ String mediaType = MIMETypeAssociations.getMIMEType(filePath);
+ if (mediaType == null) {
+ mediaType = "";
+ }
+ data = "data:" + mediaType + ";base64," + data;
+
+ return new PluginResult(PluginResult.Status.OK, data);
+ }
+
+ /**
+ * Writes data to the specified file.
+ *
+ * @param filePath
+ * Full path of file to be written to
+ * @param data
+ * Data to be written
+ * @param position
+ * Position at which to begin writing
+ * @return PluginResult containing the number of bytes written or error code
+ * if unable to write file
+ */
+ protected static PluginResult writeFile(String filePath, String data, int position) {
+ PluginResult result = null;
+ int bytesWritten = 0;
+ try {
+ // write file data
+ // The default String encoding on BB is ISO-8859-1 which causes
+ // issues with extended characters. Force to UTF-8 to provide
+ // greater character support and match other platforms.
+ bytesWritten = FileUtils.writeFile(filePath, data.getBytes("UTF-8"), position);
+ result = new PluginResult(PluginResult.Status.OK, bytesWritten);
+ }
+ catch (SecurityException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NO_MODIFICATION_ALLOWED_ERR);
+ }
+ catch (IOException e) {
+ // it's not a security issue, so the directory path is either
+ // not fully created or a general error occurred
+ Logger.log(FileManager.class.getName() + ": " + e);
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+
+ return result;
+ }
+
+ /**
+ * Changes the length of the specified file. If shortening, data beyond new
+ * length is discarded.
+ *
+ * @param fileName
+ * The full path of the file to truncate
+ * @param size
+ * The size to which the length of the file is to be adjusted
+ * @return PluginResult containing new file size or an error code if an
+ * error occurred
+ */
+ protected static PluginResult truncateFile(String filePath, long size) {
+ long fileSize = 0;
+ FileConnection fconn = null;
+ try {
+ fconn = (FileConnection) Connector.open(filePath,
+ Connector.READ_WRITE);
+ if (!fconn.exists()) {
+ Logger.log(FileManager.class.getName() + ": path not found "
+ + filePath);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ if (size >= 0) {
+ fconn.truncate(size);
+ }
+ fileSize = fconn.fileSize();
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NO_MODIFICATION_ALLOWED_ERR);
+ }
+ finally {
+ try {
+ if (fconn != null)
+ fconn.close();
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ }
+ }
+ return new PluginResult(PluginResult.Status.OK, fileSize);
+ }
+
+ /**
+ * Returns a directory entry that represents the specified file system. The
+ * directory entry does not represent the root of the file system, but a
+ * directory within the file system that is writable. Users must provide the
+ * file system type, which can be one of FS_TEMPORARY or FS_PERSISTENT.
+ *
+ * @param type
+ * The type of file system desired.
+ * @param size
+ * The minimum size, in bytes, of space required
+ * @return a PluginResult containing a file system object for the specified
+ * file system
+ */
+ protected static PluginResult requestFileSystem(int type, long size) {
+ if (!isValidFileSystemType(type)) {
+ Logger.log(FileManager.class.getName()
+ + ": Invalid file system type: " + Integer.toString(type));
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION,
+ SYNTAX_ERR);
+ }
+
+ PluginResult result = null;
+ String filePath = null;
+ switch (type) {
+ case FS_TEMPORARY:
+ // create application-specific temp directory
+ try {
+ filePath = FileUtils.createApplicationTempDirectory();
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NO_MODIFICATION_ALLOWED_ERR);
+ }
+ break;
+ case FS_PERSISTENT:
+ // get a path to SD card (if present) or user directory (internal)
+ filePath = FileUtils.getFileSystemRoot();
+ break;
+ }
+
+ // create a file system entry from the path
+ Entry entry = null;
+ try {
+ // check the file system size
+ if (size > FileUtils.availableSize(filePath)) {
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION,
+ QUOTA_EXCEEDED_ERR);
+ }
+
+ entry = getEntryFromURI(filePath);
+ }
+ catch (Exception e) {
+ // bad path (not likely)
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ ENCODING_ERR);
+ }
+
+ try {
+ JSONObject fileSystem = new JSONObject();
+ fileSystem.put("name", getFileSystemName(type));
+ fileSystem.put("root", entry.toJSONObject());
+ result = new PluginResult(PluginResult.Status.OK, fileSystem);
+ }
+ catch (JSONException e) {
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ "File system entry JSON conversion failed.");
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a file system entry object from the specified file system URI.
+ *
+ * @param uri
+ * the full path to the file or directory on the file system
+ * @return a PluginResult containing the file system entry
+ */
+ protected static PluginResult resolveFileSystemURI(String uri) {
+ PluginResult result = null;
+ Entry entry = null;
+
+ try {
+ entry = getEntryFromURI(uri);
+ }
+ catch (IllegalArgumentException e) {
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION,
+ ENCODING_ERR);
+ }
+
+ if (entry == null) {
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ else {
+ result = new PluginResult(PluginResult.Status.OK,
+ entry.toJSONObject());
+ }
+ return result;
+ }
+
+ /**
+ * Retrieve metadata for file or directory specified by path.
+ *
+ * @param path
+ * full path name of the file or directory
+ * @param full
+ * return full or partial meta data.
+ * @return PluginResult containing metadata for file system entry or an
+ * error code if unable to retrieve metadata
+ */
+ protected static PluginResult getMetadata(String path, boolean full) {
+ PluginResult result = null;
+ FileConnection fconn = null;
+ try {
+ fconn = (FileConnection)Connector.open(path);
+ if (fconn.exists()) {
+ if (full) {
+ JSONObject metadata = new JSONObject();
+ metadata.put("size", fconn.fileSize());
+ metadata.put("type",
+ MIMETypeAssociations.getMIMEType(fconn.getURL()));
+ metadata.put("name", fconn.getName());
+ metadata.put("fullPath", fconn.getURL());
+ metadata.put("lastModifiedDate", fconn.lastModified());
+ result = new PluginResult(PluginResult.Status.OK, metadata);
+ } else {
+ result = new PluginResult(PluginResult.Status.OK,
+ fconn.lastModified());
+ }
+ }
+ else {
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ }
+ catch (IllegalArgumentException e) {
+ // bad path
+ Logger.log(FileUtils.class.getName() + ": " + e);
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+ catch (IOException e) {
+ Logger.log(FileUtils.class.getName() + ": " + e);
+ result = new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_READABLE_ERR);
+ }
+ catch (JSONException e) {
+ result = new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ "File system entry JSON conversion failed.");
+ }
+ finally {
+ try {
+ if (fconn != null) fconn.close();
+ }
+ catch (IOException ignored) {
+ }
+ }
+ return result;
+ }
+
+ private static JSONObject buildEntry(String dirPath, String filePath) throws JSONException {
+ JSONObject entry = new JSONObject();
+ boolean isDir = filePath.endsWith(FileUtils.FILE_SEPARATOR);
+
+ entry.put("isFile", !isDir);
+ entry.put("isDirectory", isDir);
+ entry.put("name", isDir ? filePath.substring(0, filePath.length()-1) : filePath);
+ entry.put("fullPath", dirPath + filePath);
+
+ return entry;
+ }
+
+ /**
+ * Returns a listing of the specified directory contents. Names of both
+ * files and directories are returned.
+ *
+ * @param path
+ * full path name of directory
+ * @return PluginResult containing list of file and directory names
+ * corresponding to directory contents
+ */
+ protected static PluginResult listDirectory(String path) {
+ Enumeration listing = null;
+ try {
+ listing = FileUtils.listDirectory(path);
+ }
+ catch (Exception e) {
+ // bad path
+ Logger.log(FileUtils.class.getName() + ": " + e);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ NOT_FOUND_ERR);
+ }
+
+ try {
+ // pass directory contents back as an array of JSONObjects (entries)
+ JSONArray array = new JSONArray();
+ while (listing.hasMoreElements()) {
+ array.add(buildEntry(path, (String) listing.nextElement()));
+ }
+
+ return new PluginResult(PluginResult.Status.OK, array);
+ } catch (JSONException e) {
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+ "File system entry JSON conversion failed.");
+ }
+ }
+
+ /**
+ * Copies a file or directory to a new location. If copying a directory, the
+ * entire contents of the directory are copied recursively.
+ *
+ * @param srcPath
+ * the full path of the file or directory to be copied
+ * @param parent
+ * the full path of the target directory to which the file or
+ * directory should be copied
+ * @param newName
+ * the new name of the file or directory
+ * @return PluginResult containing an Entry object representing the new
+ * entry, or an error code if an error occurs
+ */
+ protected static PluginResult copyTo(String srcPath, String parent, String newName) {
+ try {
+ FileUtils.copy(srcPath, parent, newName);
+ }
+ catch (IllegalArgumentException e) {
+ Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION, ENCODING_ERR);
+ }
+ catch (FileNotFoundException e) {
+ Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+ }
+ catch (SecurityException e) {
+ Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, SECURITY_ERR);
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, INVALID_MODIFICATION_ERR);
+ }
+
+ return resolveFileSystemURI(getFullPath(parent, newName));
+ }
+
+ /**
+ * Moves a file or directory to a new location. If moving a directory, the
+ * entire contents of the directory are moved recursively.
+ * <p>
+ * It is an error to try to: move a directory inside itself; move a
+ * directory into its parent unless the name has changed; move a file to a
+ * path occupied by a directory; move a directory to a path occupied by a
+ * file; move any element to a path occupied by a directory that is not
+ * empty.
+ * </p>
+ * <p>
+ * A move of a file on top of an existing file must attempt to delete and
+ * replace that file. A move of a directory on top of an existing empty
+ * directory must attempt to delete and replace that directory.
+ * </p>
+ *
+ * @param srcPath
+ * the full path of the file or directory to be moved
+ * @param parent
+ * the full path of the target directory to which the file or
+ * directory should be copied
+ * @param newName
+ * the new name of the file or directory
+ * @return PluginResult containing an Entry object representing the new
+ * entry, or an error code if an error occurs
+ */
+ protected static PluginResult moveTo(String srcPath, String parent, String newName) {
+
+ // check paths
+ if (parent == null || newName == null) {
+ Logger.log(FileManager.class.getName() + ": Parameter cannot be null.");
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+ }
+ else if (!parent.endsWith(FileUtils.FILE_SEPARATOR)) {
+ parent += FileUtils.FILE_SEPARATOR;
+ }
+
+ // Rules:
+ // 1 - file replace existing file ==> OK
+ // 2 - directory replace existing EMPTY directory ==> OK
+ // 3 - file replace existing directory ==> NO!
+ // 4 - directory replace existing file ==> NO!
+ // 5 - ANYTHING replace non-empty directory ==> NO!
+ //
+ // The file-to-directory and directory-to-file checks are performed in
+ // the copy operation (below). In addition, we check the destination
+ // path to see if it is a directory that is not empty. Also, if the
+ // source and target paths have the same parent directory, it is far
+ // more efficient to rename the source.
+ //
+ FileConnection src = null;
+ FileConnection dst = null;
+ try {
+ src = (FileConnection)Connector.open(srcPath, Connector.READ_WRITE);
+ if (!src.exists()) {
+ Logger.log(FileManager.class.getName() + ": Path not found: " + srcPath);
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, NOT_FOUND_ERR);
+ }
+
+ if (src.isDirectory() && !srcPath.endsWith(FileUtils.FILE_SEPARATOR)) {
+ // Rename of a directory on OS 7+ is quirky in that it requires
+ // the opened file path to have a trailing slash.
+ src.close();
+ src = (FileConnection)Connector.open(srcPath + '/', Connector.READ_WRITE);
+ }
+
+ // cannot delete the destination path if it is a directory that is
+ // not empty
+ dst = (FileConnection) Connector.open(parent + newName, Connector.READ_WRITE);
+ if (dst.isDirectory() && dst.list("*", true).hasMoreElements()) {
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION, INVALID_MODIFICATION_ERR);
+ }
+
+ // simply rename if source path and parent are same directory
+ String srcURL = src.getURL();
+ String srcName = src.getName();
+ String srcDir = srcURL.substring(0, srcURL.length() - srcName.length());
+ if (srcDir.equals(parent)) {
+ // rename to itself is an error
+ if (FileUtils.stripSeparator(srcName).equals(
+ FileUtils.stripSeparator(newName))) {
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION,
+ INVALID_MODIFICATION_ERR);
+ }
+
+ // file replace file || directory replace directory ==> OK
+ // delete the existing entry
+ if (dst.exists() &&
+ ( (src.isDirectory() && dst.isDirectory()) ||
+ (!src.isDirectory() && !dst.isDirectory()) )) {
+ dst.delete();
+ }
+
+ // rename
+ src.rename(newName);
+ Entry entry = getEntryFromURI(parent + newName);
+ return new PluginResult(PluginResult.Status.OK, entry.toJSONObject());
+ }
+ }
+ catch (IllegalArgumentException e) {
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(
+ PluginResult.Status.JSON_EXCEPTION,
+ ENCODING_ERR);
+ }
+ catch (IOException e) {
+ // rename failed
+ Logger.log(FileManager.class.getName() + ": " + e);
+ return new PluginResult(
+ PluginResult.Status.IO_EXCEPTION,
+ INVALID_MODIFICATION_ERR);
+ }
+ finally {
+ try {
+ if (src != null) src.close();
+ if (dst != null) dst.close();
+ }
+ catch (IOException ignored) {
+ }
+ }
+
+ // There is no FileConnection API to move files and directories, so
+ // the move is a copy operation from source to destination, followed by
+ // a delete operation of the source.
+ //
+ // The following checks are made in the copy operation:
+ // * moving a directory into itself,
+ // * moving a file to an existing directory, and
+ // * moving a directory to an existing file
+ //
+ // copy source to destination
+ PluginResult result = copyTo(srcPath, parent, newName);
+
+ // if copy succeeded, delete source
+ if (result.getStatus() == PluginResult.Status.OK.ordinal()) {
+ try {
+ FileUtils.delete(srcPath);
+ }
+ catch (IOException e) {
+ // FIXME: half of move failed, but deleting either source or
+ // destination to compensate seems risky
+ Logger.log(FileManager.class.getName()
+ + ": Failed to delete source directory during move operation.");
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a file system entry for the file or directory located at the
+ * specified path.
+ *
+ * @param filePath
+ * full path name of an entry on the file system
+ * @return a file system entry corresponding to the file path, or
+ * <code>null</code> if the path is invalid or does not exist on the
+ * file system
+ * @throws IllegalArgumentException
+ * is the file path is invalid
+ * @throws IOException
+ */
+ protected static Entry getEntryFromURI(String filePath)
+ throws IllegalArgumentException {
+ // check for bogus path
+ String path = (filePath == null) ? null : filePath.trim();
+ if (path == null || path.length() < 1) {
+ throw new IllegalArgumentException("Invalid URI.");
+ }
+
+ // create a file system entry
+ Entry entry = null;
+ if (path.startsWith(FileUtils.LOCAL_PROTOCOL)) {
+ entry = getEntryFromLocalURI(filePath);
+ }
+ else {
+ FileConnection fconn = null;
+ try {
+ fconn = (FileConnection) Connector.open(path);
+ if (fconn.exists()) {
+ // create a new Entry
+ entry = new Entry();
+ entry.setDirectory(fconn.isDirectory());
+ entry.setName(FileUtils.stripSeparator(fconn.getName()));
+ entry.setFullPath(FileUtils.stripSeparator(path));
+ }
+ }
+ catch (IOException e) {
+ Logger.log(FileManager.class.getName() + ": " + e.getMessage());
+ }
+ finally {
+ try {
+ if (fconn != null) fconn.close();
+ }
+ catch (IOException ignored) {
+ }
+ }
+ }
+
+ return entry;
+ }
+
+ /**
+ * Creates a file system entry for a resource contained in the packaged
+ * application. Use this method if the specified path begins with
+ * <code>local:///</code> protocol.
+ *
+ * @param localPath
+ * the path of the application resource
+ * @return a file system entry corresponding to the local path, or
+ * <code>null</code> if a resource does not exist at the specified
+ * path
+ */
+ private static Entry getEntryFromLocalURI(String localPath) {
+ // Remove local:// from filePath but leave a leading /
+ String path = localPath.substring(8);
+ Entry entry = null;
+ if (FileUtils.FILE_SEPARATOR.equals(path)
+ || Application.class.getResourceAsStream(path) != null) {
+ entry = new Entry();
+ entry.setName(path.substring(1));
+ entry.setFullPath(localPath);
+ }
+ return entry;
+ }
+
+ /**
+ * Tests whether the specified file system type is valid.
+ *
+ * @param type
+ * file system type
+ * @return true if file system type is valid
+ */
+ protected static boolean isValidFileSystemType(int type) {
+ return (type == FS_TEMPORARY || type == FS_PERSISTENT);
+ }
+
+ /**
+ * Determines if the specified path is the root path of a file system.
+ *
+ * @param path
+ * full path
+ * @return true if the path is the root path of a file system
+ */
+ protected static boolean isFileSystemRoot(String path) {
+ if (path == null) {
+ return false;
+ }
+
+ if (!path.endsWith(FileUtils.FILE_SEPARATOR)) {
+ path += FileUtils.FILE_SEPARATOR;
+ }
+
+ boolean isRoot = false;
+ Enumeration e = FileSystemRegistry.listRoots();
+ while (e.hasMoreElements()) {
+ String root = "file:///" + (String) e.nextElement();
+ if (root.equals(path)) {
+ isRoot = true;
+ break;
+ }
+ }
+
+ return (isRoot || path.equals(FileUtils.getApplicationTempDirPath()));
+ }
+
+ /**
+ * Retrieves the name for the specified file system type.
+ *
+ * @param type
+ * file system type
+ * @return file system name
+ */
+ protected static String getFileSystemName(int type) {
+ String name = null;
+ switch (type) {
+ case FS_TEMPORARY:
+ name = "temporary";
+ break;
+ case FS_PERSISTENT:
+ name = "persistent";
+ break;
+ }
+ return name;
+ }
+
+ /**
+ * Returns full path from the directory and name specified.
+ *
+ * @param parent
+ * full path of the parent directory
+ * @param name
+ * name of the directory entry (can be <code>null</code>)
+ * @return full path of the file system entry
+ * @throws IllegalArgumentException
+ * if <code>parent</code> is <code>null</code>
+ */
+ public static String getFullPath(String parent, String name)
+ throws IllegalArgumentException {
+ if (parent == null) {
+ throw new IllegalArgumentException("Directory cannot be null.");
+ }
+
+ if (!parent.endsWith(FileUtils.FILE_SEPARATOR)) {
+ parent += FileUtils.FILE_SEPARATOR;
+ }
+ return (name == null) ? parent : parent + name;
+ }
+
+ /**
+ * Determines if the specified action should be run synchronously.
+ *
+ * @param action
+ * the action to perform
+ * @return true if the action should be synchronous
+ */
+ public boolean isSynch(String action) {
+ if (ACTION_IS_FILE_SYSTEM_ROOT.equals(action)) {
+ return true;
+ }
+ return super.isSynch(action);
+ }
+}