Saving and Deleting images via PhoneGap’s File API

The PhoneGap File API is a bit confusing to me and quite a few other people. I spent a fair amount of time looking for some “plain English” explanations and examples of storing images  but most tutorials and blog posts covered text files only. That’s great when you need to append or edit a data file but I needed to save images captured on an iPhone. It took a day of wrangling but I finally got my head wrapped around the basic API. Spent the next day building a set of methods. Let’s take a look.

I’m using PhoneGap (Cordova) 3.3 for this demo. It’s the latest stable build at the time of this writing. Seeing as PhoneGap will always be playing catch up with any changes to iOS it’s pretty important to check the docs and see if there’s been any major changes.

The Objective
We want to hit a button in the app and access the iOS photo gallery. From there we can navigate the gallery, edit a selected image, and then associate that image with a record. Pretty standard stuff for apps like Contacts, etc. This use case will need to use two of of the main API’s, Camera and File.

The Restrictions
When you call the Apple Photos App from your application the selected image file is stored in the host application’s tmp directory. This gives you a path to the file that looks something like this when you run the app in the simulator.

file:///Users/[name]/Library/Application%20Support/iPhone%20Simulator/7.0.3-64/Applications/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/tmp/cdv_photo_001.jpg.

Note the “tmp” part. That means that the file is now stored in a directory that is erased fairly often by iOS or by direct user action. If you have a reference to that file in a src attribute and the directory has been emptied to clear memory then the image breaks because it is nonexistent. When the user chooses an image we need to move (not copy) it to a persistent directory, grab the new URI, then store that new URI in the database. I say not copy because we’re looking to save drive space so it’s nice to move the file to a new location instead of creating a double.

The Code
The Phonegap best practice is to fire off your own code within the DOM ready event so we’ll start with that. When the event is fired it will log the event then run the requestFileSystem() method on line 13. The details are covered through comments.

// deviceready Event Handler
    //
    // The scope of 'this' is the event. In order to call the 'receivedEvent'
    // function, we must explicity call 'app.receivedEvent(...);'
    onDeviceReady: function() {
        app.receivedEvent('deviceready');
    },
    // Update DOM on a Received Event
    receivedEvent: function(id) {
        console.log('Received Event: ' + id);
        // request a new file system object then pass that to the gotFS() method.
        // gotFS() is a simple setter for a global var that the FileIO methods will use later.
        window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, FileIO.gotFS, FileIO.errorHandler);
    }

The second code snippet is directed to the Camera API and has three simple methods. Line 1 assigns a JQuery event handler to our button. In this case it’s been given a class of “.thumbnail-button”. When the DOM element is clicked it will execute the getPhoto() method defined on line 5. Passing the (pictureSource) parameter into the method makes the call more flexible. We’re using (pictureSource.PHOTOLIBRARY) to go directly to the iOS photo gallery and choose a photo. Had the objective been to take a new photo then we would pass (pictureSource.CAMERA) which opens the Apple Camera App instead of the Apple Photos App.

The getPhoto() method has a single call inside of it, navigator.camera.getPicture(), which takes three arguments; success callback, failure callback, and optional parameters.

I won’t get into the optional parameter set of the navigator.camera.getPicture() method because they should be fairly self explanatory. I would like to call attention to the allowEdit : true on line 11. This makes sure that we get a an image cropped to a perfect square by forcing the user into the edit mode after they tap on an image within the photo gallery and gives the user a chance to really zoom in on an area. Since we’re using these images for thumbnails we want square images with clear faces.

If the image is successfully saved the URI is passed to the onPhotoURISuccess() method on line 15 which passes that URI over to the FileIO portion of the task. We’re halfway done!


$(".thumbnail-button").on('click', function() {
getPhoto(pictureSource.PHOTOLIBRARY);
});

function getPhoto(source) {
    // Retrieve image file location from specified source
    navigator.camera.getPicture(onPhotoURISuccess, onFail, { quality: 50,
                                destinationType: destinationType.FILE_URI,
                                saveToPhotoAlbum: false,
                                sourceType: source,
                                allowEdit: true });
}

// Called when a photo is successfully retrieved
function onPhotoURISuccess(imageURI) {
    FileIO.updateCameraImages(imageURI);
}

// Called if something bad happens.
function onFail(message) {
    console.log('Failed because: ' + message);
}

Now we’ve got a new image sitting in the app’s tmp directory waiting for us to move it to a persistent location. We’ve taken the file URI of that image file and passed it over to another set of methods in the FIleIO name space. I’ve added comments to run through the sequence.

// set some globals
var gImageURI = '';
var gFileSystem = {};

var FileIO = {

// sets the filesystem to the global var gFileSystem
 gotFS : function(fileSystem) {
      gFileSystem = fileSystem;
 },

// pickup the URI from the Camera edit and assign it to the global var gImageURI
// create a filesystem object called a 'file entry' based on the image URI
// pass that file entry over to gotImageURI()
updateCameraImages : function(imageURI) {
        gImageURI = imageURI;
        window.resolveLocalFileSystemURI(imageURI, FileIO.gotImageURI, FileIO.errorHandler);
    },

// pickup the file entry, rename it, and move the file to the app's root directory.
// on success run the movedImageSuccess() method
 gotImageURI : function(fileEntry) {
       var newName = "thumbnail_" + gCurrentFlo + ".jpg";
       fileEntry.moveTo(gFileSystem.root, newName, FileIO.movedImageSuccess, FileIO.errorHandler);
 },

// send the full URI of the moved image to the updateImageSrc() method which does some DOM manipulation
 movedImageSuccess : function(fileEntry) {
      updateImageSrc(fileEntry.fullPath);
 },

// get a new file entry for the moved image when the user hits the delete button
// pass the file entry to removeFile()
 removeDeletedImage : function(imageURI){
      window.resolveLocalFileSystemURI(imageURI, FileIO.removeFile, FileIO.errorHandler);
 },

// delete the file
 removeFile : function(fileEntry){
      fileEntry.remove();
 },

// simple error handler
 errorHandler : function(e) {
       var msg = '';
       switch (e.code) {
       case FileError.QUOTA_EXCEEDED_ERR:
               msg = 'QUOTA_EXCEEDED_ERR';
               break;
        case FileError.NOT_FOUND_ERR:
               msg = 'NOT_FOUND_ERR';
               break;
        case FileError.SECURITY_ERR:
               msg = 'SECURITY_ERR';
               break;
        case FileError.INVALID_MODIFICATION_ERR:
               msg = 'INVALID_MODIFICATION_ERR';
               break;
        case FileError.INVALID_STATE_ERR:
               msg = 'INVALID_STATE_ERR';
               break;
        default:
               msg = e.code;
        break;
 };
       console.log('Error: ' + msg);
 }
}
Advertisements

9 comments

  1. Hello, Thanks for the excellent write up. Had a small question:
    In this piece of code:

    gotImageURI : function(fileEntry) {
    var newName = “thumbnail_” + gCurrentFlo + “.jpg”;
    fileEntry.moveTo(gFileSystem.root, newName, FileIO.movedImageSuccess, FileIO.errorHandler);
    },

    What is “gCurrentFlo “. Where is it defined?

  2. Hi, awesome tutorial! I see that my images got moved to Library/files folder, the good thing is that it’s the Library folder and not the Documents, but any idea how the folder could be used using cordova.file.dataDirectory so it could be placed into Library/NoCloud folder?

    thank you

  3. This post was very helpful for me, many thanks… but I think there’s a little mistake, on line 29 of the movedImageSuccess the reference to fileEntry.fullPath gives the path of the image before being moved.
    Tell me if I am right, and if I am how I can solve it.
    Thanks!!

  4. Very complete example and very well explained ! I got the same problem in my App. I used “imageURI” after call camera.getpicture and stored in SQLite then recalled them in app. Every things works until you don’t restart the app. Thank so much.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s