You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by de...@apache.org on 2012/02/08 21:14:02 UTC

[16/19] CB-226 Rename to Cordova.

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/CaptureMode.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/CaptureMode.java b/framework/ext/src/org/apache/cordova/media/CaptureMode.java
new file mode 100644
index 0000000..95bc384
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+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/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/CaptureOperation.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/CaptureOperation.java b/framework/ext/src/org/apache/cordova/media/CaptureOperation.java
new file mode 100644
index 0000000..ab647c3
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Vector;
+
+import org.apache.cordova.file.File;
+import org.apache.cordova.file.FileUtils;
+import org.apache.cordova.util.Logger;
+
+public abstract class CaptureOperation implements Runnable {
+    // max number of media files to capture
+    protected int 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(int 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/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/ImageCaptureListener.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/ImageCaptureListener.java b/framework/ext/src/org/apache/cordova/media/ImageCaptureListener.java
new file mode 100644
index 0000000..4490c27
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+import net.rim.device.api.io.file.FileSystemJournal;
+import net.rim.device.api.io.file.FileSystemJournalEntry;
+import net.rim.device.api.io.file.FileSystemJournalListener;
+
+/**
+ * Listens for image files that are added to file system.
+ * <p>
+ * The file system notifications will arrive on the application event thread.
+ * When it receives a notification, it adds the image file path to a MediaQueue
+ * so that the capture thread can process the file.
+ */
+class ImageCaptureListener implements FileSystemJournalListener {
+
+    /**
+     * Used to track file system changes.
+     */
+    private long lastUSN = 0;
+
+    /**
+     * Collection of media files.
+     */
+    private MediaQueue queue = null;
+
+    /**
+     * Constructor.
+     */
+    ImageCaptureListener(MediaQueue queue) {
+        this.queue = queue;
+    }
+
+    /**
+     * Listens for file system changes.  When a JPEG file is added, we process
+     * it and send it back.
+     */
+    public void fileJournalChanged()
+    {
+        // next sequence number file system will use
+        long USN = FileSystemJournal.getNextUSN();
+
+        for (long i = USN - 1; i >= lastUSN && i < USN; --i)
+        {
+            FileSystemJournalEntry entry = FileSystemJournal.getEntry(i);
+            if (entry == null)
+            {
+                break;
+            }
+
+            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED)
+            {
+                String path = entry.getPath();
+                if (path != null && path.indexOf(".jpg") != -1)
+                {
+                    // add file path to the capture queue
+                    queue.add("file://" + path);
+                    break;
+                }
+            }
+        }
+
+        // remember the file journal change number,
+        // so we don't search the same events again and again
+        lastUSN = USN;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/ImageCaptureOperation.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/ImageCaptureOperation.java b/framework/ext/src/org/apache/cordova/media/ImageCaptureOperation.java
new file mode 100644
index 0000000..980a1dd
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+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.file.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(int 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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/MediaCapture.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/MediaCapture.java b/framework/ext/src/org/apache/cordova/media/MediaCapture.java
new file mode 100644
index 0000000..326757a
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/MediaCapture.java
@@ -0,0 +1,504 @@
+/*
+ * 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.media;
+
+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.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";
+
+    /**
+     * 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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/MediaQueue.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/MediaQueue.java b/framework/ext/src/org/apache/cordova/media/MediaQueue.java
new file mode 100644
index 0000000..d5f5f63
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+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/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/VideoCaptureListener.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/VideoCaptureListener.java b/framework/ext/src/org/apache/cordova/media/VideoCaptureListener.java
new file mode 100644
index 0000000..269504c
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/VideoCaptureListener.java
@@ -0,0 +1,104 @@
+/*
+ * 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.media;
+
+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) {
+                    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/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/media/VideoCaptureOperation.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/media/VideoCaptureOperation.java b/framework/ext/src/org/apache/cordova/media/VideoCaptureOperation.java
new file mode 100644
index 0000000..e671585
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/media/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.media;
+
+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.file.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(int 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/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/network/ConnectionInfoAction.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/network/ConnectionInfoAction.java b/framework/ext/src/org/apache/cordova/network/ConnectionInfoAction.java
new file mode 100644
index 0000000..55a5e78
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/network/ConnectionInfoAction.java
@@ -0,0 +1,387 @@
+/*
+ * 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.network;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+import org.apache.cordova.CordovaExtension;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.json4j.JSONObject;
+import org.apache.cordova.util.Logger;
+
+import net.rim.device.api.system.Application;
+import net.rim.device.api.system.CoverageInfo;
+import net.rim.device.api.system.CoverageStatusListener;
+import net.rim.device.api.system.RadioInfo;
+import net.rim.device.api.system.RadioStatusListener;
+import net.rim.device.api.system.WLANConnectionListener;
+import net.rim.device.api.system.WLANInfo;
+
+/**
+ * Determines the current network data connection type and listens for changes
+ * to network coverage. This class is intended for use through the Network
+ * plug-in.
+ */
+public class ConnectionInfoAction {
+    // Returned JSON attributes
+    private static final String EVENT = "event";
+    private static final String TYPE = "type";
+
+    // Return types
+    private static final String TYPE_UNKNOWN = "unknown";
+    private static final String TYPE_ETHERNET = "ethernet";
+    private static final String TYPE_WIFI = "wifi";
+    private static final String TYPE_2G = "2g";
+    private static final String TYPE_3G = "3g";
+    private static final String TYPE_4G = "4g";
+    private static final String TYPE_NONE = "none";
+
+    // Network status event values
+    private static final String OFFLINE = "offline";
+    private static final String ONLINE = "online";
+
+    // Network service support constants
+    private static final int DATA = RadioInfo.NETWORK_SERVICE_DATA;
+    private static final int TWO_G = RadioInfo.NETWORK_SERVICE_DATA
+            | RadioInfo.NETWORK_SERVICE_EDGE;
+    private static final int THREE_G = RadioInfo.NETWORK_SERVICE_EVDO_REV0
+            | RadioInfo.NETWORK_SERVICE_EVDO_REV0_ONLY
+            | RadioInfo.NETWORK_SERVICE_EVDO_REVA
+            | RadioInfo.NETWORK_SERVICE_EVDO_REVA_ONLY
+            | RadioInfo.NETWORK_SERVICE_UMTS;
+
+    // Listeners used to detect network changes
+    private RadioStatusListener radioListener = null;
+    private WLANConnectionListener wlanListener = null;
+    private CoverageStatusListener coverageListener = null;
+
+    // Variable indicating whether the user has disabled mobile data
+    private Boolean dataDisabled = Boolean.FALSE;
+
+    // The set of call back IDs to send results to. Using Hashtable because
+    // BlackBerry does not support Collections. There should only ever be one
+    // call back ID, but this allows multiple.
+    private Hashtable callbackIds = new Hashtable();
+
+    private String prevType = TYPE_NONE;
+    private String prevEvent = OFFLINE;
+
+    /**
+     * Determines the current network data connection type. Listeners are
+     * registered to return additional results when network state changes.
+     *
+     * @param callbackId
+     *            The success call back ID to receive network type results.
+     * @return A PluginResult object with the success or failure result of the
+     *         operation. A success result includes information about the
+     *         currently active network type.
+     */
+    protected PluginResult getConnectionInfo(String callbackId) {
+
+        // Ensure that the dataDisabled variable is initialized.
+        setDataDisabled(CoverageInfo.getCoverageStatus(), false);
+
+        // Add the network change listeners if they have not been added.
+        addListeners(callbackId);
+
+        // Retrieve the current active connection type and build the return
+        // result.
+        String type = getConnectionType(true, true);
+        JSONObject connectInfo = new JSONObject();
+        try {
+            connectInfo.put(TYPE, type);
+        } catch (JSONException e) {
+            Logger.error("JSONException: " + e.getMessage());
+            return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                    "JSONException: " + e.getMessage());
+        }
+
+        PluginResult result = new PluginResult(PluginResult.Status.OK,
+                connectInfo);
+
+        // Need to keep the call back since listeners have been registered to
+        // fire events on the specified call back ID.
+        result.setKeepCallback(true);
+
+        return result;
+    }
+
+    /**
+     * Removes all coverage listeners and clears the list of call back IDs. This
+     * method should be invoked when the Network plug-in's onDestroy is called.
+     */
+    protected synchronized void shutdown() {
+        if (radioListener != null) {
+            Application.getApplication().removeRadioListener(radioListener);
+            radioListener = null;
+        }
+
+        if (wlanListener != null) {
+            WLANInfo.removeListener(wlanListener);
+            wlanListener = null;
+        }
+
+        if (coverageListener != null) {
+            CoverageInfo.removeListener(coverageListener);
+            coverageListener = null;
+        }
+
+        callbackIds.clear();
+    }
+
+    /**
+     * Adds a RadioStatusListener, WLANConnectionListener and
+     * CoverageStatusListener to listen for various network change events. The
+     * listeners are only registered if they have not already been added. Each
+     * listener is used to detect different types of network change events.
+     *
+     * RadioStatusListener - Detects changes in cellular data coverage.
+     * WLANConnectionListener - Detects changes in wifi coverage.
+     * CoverageStatusListener - Used to detect changes in the mobile data config
+     *
+     * @param callbackId
+     *            The reference point to call back when a listener event occurs.
+     */
+    private synchronized void addListeners(String callbackId) {
+        callbackIds.put(callbackId, callbackId);
+
+        if (radioListener == null) {
+            radioListener = new RadioStatusListener() {
+                public void baseStationChange() {}
+                public void networkScanComplete(boolean success) {}
+
+                public void networkServiceChange(int networkId, int service) {
+                    // Cellular data change detected. If the user hasn't
+                    // disabled mobile data and wifi is not currently in use
+                    // return a result indicating the cellular data coverage
+                    // change.
+                    if (dataDisabled == Boolean.FALSE
+                            && WLANInfo.getWLANState() != WLANInfo.WLAN_STATE_CONNECTED) {
+                        if ((service & DATA) == 0) {
+                            sendResult(TYPE_NONE, OFFLINE);
+                        } else {
+                            // In the case where cell data and wifi was turned
+                            // off and then the user disabled mobile data
+                            // configuration, the mobile data config disablement
+                            // by the user isn't detected by the coverage status
+                            // listener so dataDisabled may not be accurate.
+                            // When service data is activated, have to make sure
+                            // that dataDisabled is properly set.
+                            setDataDisabled(CoverageInfo.getCoverageStatus(),
+                                    false);
+                            if (dataDisabled == Boolean.FALSE) {
+                                sendResult(getConnectionType(false, true),
+                                        ONLINE);
+                            }
+                        }
+                    }
+                }
+
+                public void networkStarted(int networkId, int service) {}
+                public void networkStateChange(int state) {}
+                public void pdpStateChange(int apn, int state, int cause) {}
+                public void radioTurnedOff() {}
+                public void signalLevel(int level) {}
+            };
+            Application.getApplication().addRadioListener(radioListener);
+        }
+
+        if (wlanListener == null) {
+            wlanListener = new WLANConnectionListener() {
+                public void networkConnected() {
+                    if (dataDisabled == Boolean.FALSE) {
+                        sendResult(TYPE_WIFI, ONLINE);
+                    }
+                }
+
+                public void networkDisconnected(int reason) {
+                    // Wifi was disconnected, if the user hasn't disabled mobile
+                    // data, check if cellular data coverage exists.
+                    if (dataDisabled == Boolean.FALSE) {
+                        String type = getConnectionType(false, true);
+                        String event = OFFLINE;
+                        if (!TYPE_NONE.equals(type)) {
+                            event = ONLINE;
+                        }
+                        sendResult(type, event);
+                    }
+                }
+            };
+            WLANInfo.addListener(wlanListener);
+        }
+
+        if (coverageListener == null) {
+            coverageListener = new CoverageStatusListener() {
+                public void coverageStatusChanged(int newCoverage) {
+                    // When coverage changes, check to determine if it is due
+                    // to the user disabling mobile data through configuration
+                    // flag.
+                    setDataDisabled(newCoverage, true);
+                }
+            };
+            CoverageInfo.addListener(coverageListener);
+        }
+    }
+
+    /**
+     * Determine the type of connection currently being used for data
+     * transmission on the device. If the user has disabled mobile data then
+     * TYPE_NONE is returned regardless of cellular or wifi radio state as this
+     * is the way the browser behaves. Otherwise, wifi and/or cellular radios
+     * are queried for state based on the passed in parameters.
+     *
+     * @param checkWLAN
+     *            Determines whether wifi radio state is queried or not.
+     * @param checkCell
+     *            Determines whether cellular radio state is queried or not.
+     * @return A string indicating one of the defined network connections types.
+     */
+    private String getConnectionType(boolean checkWLAN, boolean checkCell) {
+        String networkType = TYPE_NONE;
+
+        if (dataDisabled == Boolean.FALSE) {
+            // Detect if wifi is active and connected. If wifi is active it
+            // takes precedence over cellular data transmission.
+            if (checkWLAN
+                    && WLANInfo.getWLANState() == WLANInfo.WLAN_STATE_CONNECTED) {
+                networkType = TYPE_WIFI;
+            }
+
+            if (checkCell && TYPE_NONE.equals(networkType)
+                    && RadioInfo.isDataServiceOperational()) {
+
+                int activeServices = RadioInfo.getNetworkService();
+                networkType = TYPE_UNKNOWN;
+                if ((activeServices & THREE_G) != 0) {
+                    networkType = TYPE_3G;
+                } else if ((activeServices & TWO_G) != 0) {
+                    networkType = TYPE_2G;
+                }
+            }
+        }
+
+        return networkType;
+    }
+
+    /**
+     * Helper function to build and send the PluginResult to the saved call back
+     * IDs.
+     *
+     * @param type
+     *            The network connection type. This value should be null if the
+     *            specified event is "offline".
+     * @param event
+     *            The network event.
+     */
+    private void sendResult(String type, String event) {
+
+        JSONObject connectionInfo = new JSONObject();
+        try {
+            connectionInfo.put(TYPE, type);
+            connectionInfo.put(EVENT, event);
+        } catch (JSONException e) {
+            Logger.error("JSONException: " + e.getMessage());
+            for (Enumeration callbacks = this.callbackIds.elements(); callbacks
+                    .hasMoreElements();) {
+                String callbackId = (String) callbacks.nextElement();
+                CordovaExtension.invokeErrorCallback(callbackId,
+                        new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                                "JSONException: " + e.getMessage()));
+            }
+            return;
+        }
+
+        // Only send the event if it is different then the last sent event.
+        synchronized (prevType) {
+            if (prevType != null && prevEvent != null && prevType.equals(type)
+                    && prevEvent.equals(event)) {
+                return;
+            } else {
+                prevType = type;
+                prevEvent = event;
+            }
+        }
+
+        PluginResult result = new PluginResult(PluginResult.Status.OK,
+                connectionInfo);
+
+        // Must keep the call back active for future events.
+        result.setKeepCallback(true);
+
+        // Iterate through the saved call back IDs. Really should only ever be
+        // one.
+        for (Enumeration callbacks = this.callbackIds.elements(); callbacks
+                .hasMoreElements();) {
+            String callbackId = (String) callbacks.nextElement();
+            CordovaExtension.invokeSuccessCallback(callbackId, result);
+        }
+
+    }
+
+    /**
+     * Determines if the user has disabled mobile data through the user level
+     * configuration panels and optionally returns an "online" or "offline"
+     * result.
+     *
+     * @param newCoverage
+     *            A bit mask of CoverageInfo.COVERAGE_* flags indicating the
+     *            current coverage.
+     * @param returnResult
+     *            If true, return a result based on the value of the mobile data
+     *            configuration.
+     */
+    private void setDataDisabled(int newCoverage, boolean returnResult) {
+
+        boolean isRadioData = (RadioInfo.getNetworkService() & DATA) != 0;
+        boolean wlanConnected = WLANInfo.getWLANState() == WLANInfo.WLAN_STATE_CONNECTED;
+        boolean eventDetected = false;
+        String event = OFFLINE;
+
+        // Note: To detect that mobile data has been disabled through
+        // configuration, determine if the current coverage is
+        // CoverageInfo.COVERAGE_NONE AND that either cellular or wifi radios
+        // are currently connected. This is because the low level radio routines
+        // will return a connected state even when mobile data is disabled
+        // through configuration.
+        synchronized (dataDisabled) {
+            if (newCoverage == CoverageInfo.COVERAGE_NONE
+                    && (isRadioData || wlanConnected)) {
+                if (dataDisabled == Boolean.FALSE) {
+                    Logger.log("Mobile data was disabled by the user through configuration.");
+                    dataDisabled = Boolean.TRUE;
+                    eventDetected = true;
+                }
+            } else if (dataDisabled == Boolean.TRUE) {
+                Logger.log("Mobile data was enabled by the user.");
+                dataDisabled = Boolean.FALSE;
+                event = ONLINE;
+                eventDetected = true;
+            }
+        }
+
+        if (returnResult && eventDetected) {
+            // The user has enabled/disabled mobile data. Return a result
+            // indicating the current network state.
+            String type = getConnectionType(true, true);
+            sendResult(type, event);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/network/Network.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/network/Network.java b/framework/ext/src/org/apache/cordova/network/Network.java
new file mode 100644
index 0000000..86e5ac6
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/network/Network.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.network;
+
+import org.apache.cordova.api.Plugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONArray;
+
+/**
+ * The Network command interface.
+ *
+ * The Network class can invoke the following actions:
+ *
+ *   - getConnectionInfo(callback)
+ *
+ */
+public class Network extends Plugin {
+	// Supported actions
+	public static final String ACTION_CONNECTION_INFO = "getConnectionInfo";
+
+	private ConnectionInfoAction connectionInfo = new ConnectionInfoAction();
+
+	/**
+	 * Executes the request and returns CommandResult.
+	 *
+	 * @param action The command to execute.
+	 * @param callbackId The callback ID to be invoked upon action completion
+	 * @param args   JSONArry of arguments for the command.
+	 * @return A CommandResult object with a status and message.
+	 */
+	public PluginResult execute(String action, JSONArray args, String callbackId) {
+		PluginResult result = null;
+
+		if (action.equals(ACTION_CONNECTION_INFO)) {
+			result = connectionInfo.getConnectionInfo(callbackId);
+		}
+		else {
+			result = new PluginResult(PluginResult.Status.INVALID_ACTION, "Network: Invalid action: " + action);
+		}
+
+		return result;
+	}
+
+	/**
+	 * Called when Plugin is destroyed.
+	 */
+	public void onDestroy() {
+		connectionInfo.shutdown();
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/notification/ActivityDialog.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/notification/ActivityDialog.java b/framework/ext/src/org/apache/cordova/notification/ActivityDialog.java
new file mode 100644
index 0000000..710f61e
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/notification/ActivityDialog.java
@@ -0,0 +1,146 @@
+/*
+ * 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.notification;
+
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+import org.apache.cordova.ui.SpacerField;
+
+import net.rim.device.api.system.Bitmap;
+import net.rim.device.api.system.Characters;
+import net.rim.device.api.ui.UiApplication;
+import net.rim.device.api.ui.component.BitmapField;
+import net.rim.device.api.ui.component.LabelField;
+import net.rim.device.api.ui.component.SeparatorField;
+import net.rim.device.api.ui.container.HorizontalFieldManager;
+import net.rim.device.api.ui.container.PopupScreen;
+import net.rim.device.api.ui.container.VerticalFieldManager;
+
+/**
+ * A Popup activity dialog box with an optional title and message.
+ */
+public final class ActivityDialog extends PopupScreen {
+    private static ActivityDialog dialog = null;
+
+    /**
+     * Construct an activity dialog, with customizable title and message.
+     *
+     * @param title
+     *            Title of the activity dialog
+     * @param message
+     *            Message to print in the body of the dialog
+     */
+    private ActivityDialog(String title, String message) {
+        super(new VerticalFieldManager());
+
+        if (title != null && title.length() > 0) {
+            add(new LabelField(title));
+            add(new SeparatorField(SeparatorField.LINE_HORIZONTAL));
+        }
+        add(new SpacerField(0, 20));
+
+        HorizontalFieldManager hfm = new HorizontalFieldManager();
+
+        Bitmap hourglass = Bitmap.getPredefinedBitmap(Bitmap.HOURGLASS);
+
+        BitmapField bitmap = new BitmapField(hourglass);
+        hfm.add(bitmap);
+
+        if (message != null && message.length() > 0) {
+            hfm.add(new LabelField(message, FIELD_HCENTER | FIELD_VCENTER));
+        }
+
+        add(hfm);
+        add(new SpacerField(0, 20));
+    }
+
+    /**
+     * Creates and displays the activity dialog.
+     *
+     * @param args
+     *            JSONArray of arguments.
+     * @return a PluginResult indicating success or error.
+     */
+    static synchronized PluginResult start(JSONArray args) {
+        if (dialog == null) {
+            String message = null;
+            String title = null;
+
+            // Title and message are optional, grab the strings from the args
+            // if they are there.
+            if (args != null && args.length() > 0) {
+                try {
+                    if (!args.isNull(0)) {
+                        title = args.getString(0);
+                    }
+                    if (args.length() > 1 && !args.isNull(1)) {
+                        message = args.getString(1);
+                    }
+                } catch (JSONException e) {
+                    return new PluginResult(PluginResult.Status.JSON_EXCEPTION,
+                            "JSONException: " + e.getMessage());
+                }
+            }
+
+            dialog = new ActivityDialog(title, message);
+            final UiApplication uiApp = UiApplication.getUiApplication();
+            uiApp.invokeLater(new Runnable() {
+                public void run() {
+                    uiApp.pushModalScreen(dialog);
+                }
+            });
+        }
+
+        return new PluginResult(PluginResult.Status.OK, "");
+    }
+
+    /**
+     * Closes the activity dialog.
+     *
+     * @return a PluginResult indicating success or error.
+     */
+    static synchronized PluginResult stop() {
+        if (dialog != null) {
+            final UiApplication uiApp = UiApplication.getUiApplication();
+            final ActivityDialog tmpDialog = dialog;
+            uiApp.invokeLater(new Runnable() {
+                public void run() {
+                    uiApp.popScreen(tmpDialog);
+                }
+            });
+            dialog = null;
+        }
+
+        return new PluginResult(PluginResult.Status.OK, "");
+    }
+
+    /**
+     * @see net.rim.device.api.ui.Screen#keyChar(char, int, int)
+     */
+    protected boolean keyChar(char key, int status, int time) {
+        // If the user clicks back key while progress dialog is displayed, close
+        // the progress dialog.
+        if (key == Characters.ESCAPE) {
+            stop();
+        }
+
+        return super.keyChar(key, status, time);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/notification/AlertAction.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/notification/AlertAction.java b/framework/ext/src/org/apache/cordova/notification/AlertAction.java
new file mode 100644
index 0000000..c04a3cb
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/notification/AlertAction.java
@@ -0,0 +1,83 @@
+/*
+ * 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.notification;
+
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.json4j.JSONArray;
+import org.apache.cordova.json4j.JSONException;
+
+import net.rim.device.api.ui.UiApplication;
+
+/**
+ * Alert Action
+ *
+ * Displays and interacts with a dialog box.
+ *
+ */
+public class AlertAction {
+
+    private static final String DEFAULT_MESSAGE = "";
+    private static final String DEFAULT_TITLE   = "Alert";
+    private static final String DEFAULT_BUTTON  = "OK";
+
+    /**
+     * Displays a custom alert.
+     *
+     * @param args JSONArray formatted as [ message, title, buttonLabel ]
+     *             message:     the message to display in the dialog body (default: "").
+     *             title:       the title to display at the top of the dialog (default: "Alert").
+     *             buttonLabel: the button text (default: "OK").
+     * @return A PluginResult object with the success or failure state for displaying the dialog box.
+     */
+    public static PluginResult execute(JSONArray args) {
+
+        PluginResult result = null;
+
+        try {
+            String message = DEFAULT_MESSAGE;
+            String title = DEFAULT_TITLE;
+            String buttonLabel = DEFAULT_BUTTON;
+            if (args.length() > 0 && !args.isNull(0))
+                message = args.getString(0);
+            if (args.length() > 1 && !args.isNull(1))
+                title = args.getString(1);
+            if (args.length() > 2 && !args.isNull(2))
+                buttonLabel = args.getString(2);
+
+            // construct the dialog
+            final AlertDialog dialog = new AlertDialog(message, title, buttonLabel);
+
+            // ask the event dispatch thread to show dialog
+            Runnable runnable = new Runnable() {
+                public void run() {
+                    UiApplication ui = UiApplication.getUiApplication();
+                    ui.pushModalScreen(dialog);
+                }
+            };
+            UiApplication.getUiApplication().invokeAndWait(runnable);
+
+            result = new PluginResult(PluginResult.Status.OK);
+        }
+        catch (JSONException e) {
+            result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, "JSONException: " + e.getMessage());
+        }
+
+        return result;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/notification/AlertDialog.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/notification/AlertDialog.java b/framework/ext/src/org/apache/cordova/notification/AlertDialog.java
new file mode 100644
index 0000000..a32666b
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/notification/AlertDialog.java
@@ -0,0 +1,68 @@
+/*
+ * 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.notification;
+
+import org.apache.cordova.ui.SpacerField;
+
+import net.rim.device.api.ui.Field;
+import net.rim.device.api.ui.FieldChangeListener;
+import net.rim.device.api.ui.component.ButtonField;
+import net.rim.device.api.ui.component.LabelField;
+import net.rim.device.api.ui.component.SeparatorField;
+import net.rim.device.api.ui.container.PopupScreen;
+import net.rim.device.api.ui.container.VerticalFieldManager;
+
+public final class AlertDialog extends PopupScreen implements FieldChangeListener {
+
+    private ButtonField button;
+
+    /**
+     * Open a custom alert dialog, with a customizable title and button text.
+     *
+     * @param {String} message Message to print in the body of the alert
+     * @param {String} title Title of the alert dialog (default: 'Alert')
+     * @param {String} buttonLabel Label of the close button (default: 'OK')
+     */
+    public AlertDialog(String message, String title, String buttonLabel) {
+
+        super(new VerticalFieldManager());
+
+        // title
+        add(new LabelField(title));
+
+        // separator
+        add(new SeparatorField(SeparatorField.LINE_HORIZONTAL));
+
+        // message
+        add(new SpacerField(0, 20));
+        add(new LabelField(message, FIELD_HCENTER | FIELD_VCENTER));
+        add(new SpacerField(0, 20));
+
+        // button
+        button = new ButtonField(buttonLabel, ButtonField.CONSUME_CLICK | FIELD_HCENTER);
+        button.setChangeListener(this);
+        add(button);
+    }
+
+    public void fieldChanged(Field field, int context) {
+        if (button == field) {
+            close();
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-blackberry-webworks/blob/05319d3c/framework/ext/src/org/apache/cordova/notification/BeepAction.java
----------------------------------------------------------------------
diff --git a/framework/ext/src/org/apache/cordova/notification/BeepAction.java b/framework/ext/src/org/apache/cordova/notification/BeepAction.java
new file mode 100644
index 0000000..b1c3658
--- /dev/null
+++ b/framework/ext/src/org/apache/cordova/notification/BeepAction.java
@@ -0,0 +1,160 @@
+/*
+ * 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.notification;
+
+import java.io.IOException;
+
+import javax.microedition.media.Control;
+import javax.microedition.media.Manager;
+import javax.microedition.media.MediaException;
+import javax.microedition.media.Player;
+import javax.microedition.media.control.ToneControl;
+import javax.microedition.media.control.VolumeControl;
+
+import org.apache.cordova.CordovaExtension;
+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.device.api.media.control.AudioPathControl;
+import net.rim.device.api.notification.NotificationsManager;
+import net.rim.device.api.system.Alert;
+
+/**
+ * Beep Action plays a device tone.
+ */
+public class BeepAction {
+
+    private static final int BEEP_VOLUME = 100;
+    private static final byte NOTE = 76;                // E5
+    private static final byte D = 16;                   // quarter note
+    private static final byte tempo = 30;               // 120 bpm
+
+    private static final byte[] TONE_SEQUENCE_START = {
+            ToneControl.VERSION, 1,
+            ToneControl.TEMPO, tempo
+    };
+
+    private static final byte[] TONE = {
+        NOTE,D, ToneControl.SILENCE,D/2
+	};
+
+    /**
+     * Beeps the device for a given number of times.  By default, it will beep
+     * once.  If the user explicitly sets the beep count to zero, it will play
+     * the applications notification profile.  The application profile playback
+     * sequence is controlled by the user.
+     *
+     * @param args JSONArray formatted as [ count ]
+     *             count: specifies the number of times to beep the device (default: 1).
+     * @return A CommandResult object with the success or failure
+     *         state for beeping the device.
+     */
+    public static PluginResult execute(JSONArray args) {
+        PluginResult result = null;
+
+        if (Alert.isAudioSupported()) {
+
+            try {
+                int repeatCount = 1;
+                if (args.length() > 0 && !args.isNull(0)) {
+                    repeatCount = args.getInt(0);
+                }
+
+                // play tone n times
+                if (repeatCount > 0) {
+                    playTone(repeatCount);
+                }
+                // FIXME: unsupported on other platforms
+                // send notification event to application profile
+                else {
+                    NotificationsManager.triggerImmediateEvent(
+                            CordovaExtension.getAppID(), 0, null, null);
+                }
+            }
+            catch (JSONException e) {
+                Logger.log(BeepAction.class.getName() + ": " + e);
+                result = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
+            }
+            catch (Exception e) {
+                Logger.log(BeepAction.class.getName() + ": " + e);
+                result = new PluginResult(PluginResult.Status.IO_EXCEPTION, e.getMessage());
+            }
+
+            result = new PluginResult(PluginResult.Status.OK);
+        }
+        else {
+            result = new PluginResult(PluginResult.Status.ILLEGAL_ACCESS_EXCEPTION, "Audio not supported");
+        }
+
+        return result;
+    }
+
+    /**
+     * Plays a tone the specified number of times on the device audio system.
+     * @param repeatCount number of times to play tone
+     */
+    private static void playTone(int repeatCount) throws MediaException, IOException {
+
+        // get tone player
+        Player p = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
+        p.realize();
+
+        // set tone sequence
+        ToneControl tc = (ToneControl)p.getControl("ToneControl");
+        tc.setSequence(getToneSequence(repeatCount));
+
+        // crank up the volume
+        VolumeControl vc = (VolumeControl)p.getControl("VolumeControl");
+        vc.setLevel(BEEP_VOLUME);
+
+        // route audio to speaker phone
+        p.prefetch();
+        Control[] c = p.getControls();
+        for(int i=c.length-1; i>=0; --i) {
+            if(c[i] instanceof AudioPathControl) {
+                AudioPathControl apc = (AudioPathControl)c[i];
+                apc.setAudioPath(AudioPathControl.AUDIO_PATH_HANDSFREE);
+                break;
+            }
+        }
+
+        // play
+        p.start();
+    }
+
+    /**
+     * Creates a tone sequence to play.
+     * @param repeatCount number of times to play tone
+     * @return tone sequence
+     */
+    private static byte[] getToneSequence(int repeatCount) {
+        // we have to build the sequence dynamically because
+        // ToneControl.REPEAT, repeatCount, TONE, DURATION
+        // doesn't seem to work
+        byte[] sequence = new byte[TONE_SEQUENCE_START.length + TONE.length * repeatCount];
+        System.arraycopy(TONE_SEQUENCE_START, 0, sequence, 0, TONE_SEQUENCE_START.length);
+        for (int i = 0; i < repeatCount; i++) {
+            System.arraycopy(TONE, 0, sequence, (TONE_SEQUENCE_START.length+TONE.length*i), TONE.length);
+        }
+
+        return sequence;
+    }
+}