com.phonegap.camera.CapturePhotoAction.java Source code

Java tutorial

Introduction

Here is the source code for com.phonegap.camera.CapturePhotoAction.java

Source

/*
 * PhoneGap is available under *either* the terms of the modified BSD license *or* the
 * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
 * 
 * Copyright (c) 2005-2010, Nitobi Software Inc.
 * Copyright (c) 2010, IBM Corporation
 */
package com.phonegap.camera;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;

import net.rim.blackberry.api.invoke.CameraArguments;
import net.rim.blackberry.api.invoke.Invoke;
import net.rim.device.api.io.Base64OutputStream;
import net.rim.device.api.io.file.FileSystemJournal;
import net.rim.device.api.io.file.FileSystemJournalEntry;
import net.rim.device.api.io.file.FileSystemJournalListener;
import net.rim.device.api.system.Characters;
import net.rim.device.api.system.EventInjector;
import net.rim.device.api.ui.UiApplication;

import org.json.me.JSONArray;

import com.phonegap.PhoneGapExtension;
import com.phonegap.api.PluginResult;
import com.phonegap.util.Logger;

public class CapturePhotoAction implements FileSystemJournalListener {
    private static final int DATA_URL = 0;
    private static final int FILE_URI = 1;

    /**
     * Maximum image encoding size (in bytes).  Obtained unofficially through trial and error.
     */
    private static final long MAX_ENCODING_SIZE = 1500000L;

    private long lastUSN = 0;
    private String callbackId;
    private int destinationType = DATA_URL;

    public CapturePhotoAction(String callbackId) {
        this.callbackId = callbackId;
    }

    /**
     * Capture a photo using device camera.  The camera is invoked using native APIs.
     * The photo is captured by listening to file system changes, and sent back by 
     * invoking the appropriate JS callback.
     *
     * @param args JSONArray formatted as [ cameraArgs ]
     *        cameraArgs:      
      *          [ 80,                                    // quality (ignored)
      *            Camera.DestinationType.DATA_URL,       // destinationType
      *            Camera.PictureSourceType.PHOTOLIBRARY  // sourceType (ignored)]
     * @return A CommandResult object with the INPROGRESS state for taking a photo.
     */
    public PluginResult execute(JSONArray args) {
        // get the camera options, if supplied
        if (args != null && args.length() > 1) {
            // determine the desired destination type: data or file URI
            Integer destType = (Integer) args.opt(1);
            this.destinationType = (destType != null && destType.intValue() == FILE_URI) ? FILE_URI : DATA_URL;
        }

        // MMAPI interface doesn't use the native Camera application or interface
        // (we would have to replicate it).  So, we invoke the native Camera application,
        // which doesn't allow us to set any options.
        synchronized (UiApplication.getEventLock()) {
            UiApplication.getUiApplication().addFileSystemJournalListener(this);
            Invoke.invokeApplication(Invoke.APP_TYPE_CAMERA, new CameraArguments());
        }

        // We invoked the native camera application, which runs in a separate
        // process, and must now wait for the listener to retrieve the photo taken. 
        // Return NO_RESULT status so plugin manager does not invoke a callback,
        // but set keep callback to true so we can invoke the callback later.
        PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
        result.setKeepCallback(true);
        return result;
    }

    /**
     * Listens for file system changes.  When a JPEG file is added, we process 
     * it and send it back.
     */
    public void fileJournalChanged() {
        // next sequence number file system will use
        long USN = FileSystemJournal.getNextUSN();

        for (long i = USN - 1; i >= lastUSN && i < USN; --i) {
            FileSystemJournalEntry entry = FileSystemJournal.getEntry(i);
            if (entry == null) {
                break;
            }

            if (entry.getEvent() == FileSystemJournalEntry.FILE_ADDED) {
                String path = entry.getPath();
                if (path != null && path.indexOf(".jpg") != -1) {
                    // we found a new JPEG, process it
                    final PluginResult result = processImage("file://" + path);

                    // Invoking the callback to the JavaScript engine seems to necessitate 
                    // a new thread in Blackberry OS 6.0.  This was unnecessary in 5.0, 
                    // but there is a different threading model with the new WebKit engine 
                    // in 6.0.  Invoking the JS callback in the same thread causes the 
                    // application to crash in 6.0.
                    Thread thread = new Thread(new Runnable() {
                        public void run() {
                            // invoke the appropriate callback
                            if (result.getStatus() == PluginResult.Status.OK.ordinal()) {
                                PhoneGapExtension.invokeSuccessCallback(callbackId, result);
                            } else {
                                PhoneGapExtension.invokeErrorCallback(callbackId, result);
                            }
                        }
                    });
                    thread.start();

                    // clean up
                    closeCamera();

                    break;
                }
            }
        }

        // remember the file journal change number, 
        // so we don't search the same events again and again
        lastUSN = USN;
    }

    /**
     * Returns either the image URI or the image itself encoded as a Base64 string.
     */
    private PluginResult processImage(String photoPath) {
        Logger.log(this.getClass().getName() + ": processing image " + photoPath);
        String resultData;

        if (this.destinationType == FILE_URI) {
            // just return the photo URI
            resultData = photoPath;
        } else {
            // encode the image as base64 string
            try {
                resultData = encodeImage(photoPath);
            } catch (Exception e) {
                return new PluginResult(PluginResult.Status.IOEXCEPTION, e.toString());
            }

            // we have to check the size to avoid memory errors in the browser
            if (resultData.length() > MAX_ENCODING_SIZE) {
                // it's a big one.  this is for your own good.
                String msg = "Encoded image is too large. Try reducing camera image quality.";
                Logger.log(this.getClass().getName() + ": " + msg);
                return new PluginResult(PluginResult.Status.ERROR, msg);
            }
        }

        return new PluginResult(PluginResult.Status.OK, resultData);
    }

    /**
     * Opens the photo at a given URI and converts it to a Base64-encoded string.
     * @param photoPath - file URI
     * @return Base64-encoded String of image at file URI
     */
    private String encodeImage(String photoPath) throws IOException {
        String imageData = null;

        // open the image file
        FileConnection fconn = (FileConnection) Connector.open(photoPath);
        try {
            if (fconn.exists()) {
                InputStream imageStream = fconn.openInputStream();

                // read the image data
                int size = 100000;
                int imageSize = 0;
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                Base64OutputStream base64OutputStream = new Base64OutputStream(byteArrayOutputStream);
                byte[] buffer = new byte[size];
                while (true) {
                    int bytesRead = imageStream.read(buffer, 0, size);
                    imageSize += bytesRead;
                    if (imageSize > 0 && bytesRead == -1)
                        break;
                    base64OutputStream.write(buffer, 0, bytesRead);
                }

                base64OutputStream.flush();
                base64OutputStream.close();
                imageData = byteArrayOutputStream.toString();
                imageStream.close();

                Logger.log(this.getClass().getName() + ": image size=" + Integer.toString(imageSize));
                Logger.log(this.getClass().getName() + ": Base64 encoding size="
                        + Integer.toString(imageData.length()));
            }
        } finally {
            fconn.close();
        }

        return imageData;
    }

    /**
     * Closes the native camera application. 
     */
    private void closeCamera() {
        // cleanup - remove file system listener
        UiApplication.getUiApplication().removeFileSystemJournalListener(this);

        // simulate two escape characters to exit camera application
        // no, there is no other way to do this
        EventInjector.KeyEvent inject = new EventInjector.KeyEvent(EventInjector.KeyEvent.KEY_DOWN,
                Characters.ESCAPE, 0);
        inject.post();
        inject.post();
    }
}