PhoneGap Sample - Diary (Database and Camera support)
Join the DZone community and get the full member experience.
Join For FreeLast week a reader wrote in with an interesting problem. They had a simple application that made use of PhoneGap's database support. They wanted to add camera support as well. Their idea was that a picture could be associated with their content. But then they ran into this issue:
iOS Quirks:
When destinationType.FILE_URI is used, photos are saved in the application's temporary directory. Developers may delete the contents of this directory using the navigator.fileMgr APIs if storage space is a concern.
So, right away, this brings up a useful reminder. Most things you do with PhoneGap will work cross platform, but it is crucial that you pay attention to the individual quirks for each platform. In this case, something that works fine in Android will cause problems in iOS. Even worse, because the images aren't removed immediately, you may think there isn't a problem at all. (As a side note, navigator.fileMgr is old code that has not been removed from the docs yet. I've filed a bug report to get this updated.)
I thought I'd help out the reader by building a simple application that made use of both technologies and then work out how I'd handle the iOS issue. I started off by building a simple Diary application. The Diary would allow you to write basic content entries, each with a title, body, and a creation date. I built a very simple "Single Page Architecture" framework to handle my application views and routing. I won't even call it a framework. Really it is just one simple JavaScript function that lets me load a page into the DOM.
Here's the home page - a simple list of entries with the ability to view an entry and add a new entry.
And the amazingly well-designed entry page:
Finally, the form to write entries:
I also built a wrapper for my Diary class that would abstract out the persistence for me. Here is that wrapper. Please don't laugh at my pitiful object-oriented-ish JavaScript code.
function Diary() { that = this; } Diary.prototype.setup = function(callback) { //First, setup the database this.db = window.openDatabase("diary", 1, "diary", 1000000); this.db.transaction(this.initDB, this.dbErrorHandler, callback); } //Geenric database error handler. Won't do anything for now. Diary.prototype.dbErrorHandler = function(e) { console.log('DB Error'); console.dir(e); } //I initialize the database structure Diary.prototype.initDB = function(t) { t.executeSql('create table if not exists diary(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, body TEXT, image TEXT, published DATE)'); } Diary.prototype.getEntries = function(start,callback) { console.log('Running getEntries'); if(arguments.length === 1) callback = arguments[0]; this.db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary order by published desc',[], function(t,results) { callback(that.fixResults(results)); },this.dbErrorHandler); }, this.dbErrorHandler); } Diary.prototype.getEntry = function(id, callback) { this.db.transaction( function(t) { t.executeSql('select id, title, body, image, published from diary where id = ?', [id], function(t, results) { callback(that.fixResult(results)); }, this.dbErrorHandler); }, this.dbErrorHandler); } //No support for edits yet Diary.prototype.saveEntry = function(data, callback) { console.dir(data); this.db.transaction( function(t) { t.executeSql('insert into diary(title,body,published) values(?,?,?)', [data.title, data.body, new Date().getTime()], function() { callback(); }, this.dbErrorHandler); }, this.dbErrorHandler); } //Utility to convert record sets into array of obs Diary.prototype.fixResults = function(res) { var result = []; for(var i=0, len=res.rows.length; i<len; i++) { var row = res.rows.item(i); result.push(row); } return result; } //I'm a lot like fixResults, but I'm only used in the context of expecting one row, so I return an ob, not an array Diary.prototype.fixResult = function(res) { if(res.rows.length) { return res.rows.item(0); } else return {}; }
This wrapper class is used by index.js, which handles my views, adding and requesting data, etc. Basically, index.js acts like a controller in your typical MVC setup. I'm not going to share all of that file (to be clear, everything is shared via a Download link below), but here is an example.
$(document).on("pageload", "#mainPage", function(e) { diary.getEntries(function(data) { console.log('getEntries'); var s = ""; for(var i=0, len=data.length; i<len; i++) { s += "<div data-id='"+data[i].id+"'>" + data[i].title + "</div>"; } $("#entryList").html(s); //Listen for add clicks $("#addEntryBtn").on("touchend", function(e) { e.preventDefault(); pageLoad("add.html"); }); //Listen for entry clicks $("#entryList div").on("touchend", function(e) { e.preventDefault(); var id = $(this).data("id"); pageLoad("entry.html?id="+id); }); }); });
So - nothing too terribly complex. I took this as a starting point (in the zip you can download, this may be found in www1) and then began to integrate camera functionality. I started by adding a new button to my entry field. This button, "Add Picture", would request the device camera and store the resulting image in a hidden form field. I began by using FILE_URIs even though I knew it would be an issue with iOS. In almost everything I do I start slowly, take baby steps, and try to build one thing at a time. So with that in mind, I went ahead and just built it in.
Here is that code:
$(document).on("pageload", "#addPage", function(e) { function onCamSuccess(imgdata) { console.log(imgdata); $("#entryPicture").val(imgdata); $("#imgPreview").attr("src", imgdata); } function onCamFail(e) { console.log('camFail');console.dir(e); navigator.notification.alert("Sorry, something went wrong.", null, "Oops!"); } $("#takePicture").on("touchstart", function(e) { e.preventDefault(); navigator.camera.getPicture(onCamSuccess, onCamFail, {quality:50, destinationType:Camera.DestinationType.FILE_URI}); }); $("#addEntrySubmit").on("touchstart", function(e) { e.preventDefault(); //grab the values var title = $("#entryTitle").val(); var body = $("#entryBody").val(); var img = $("#entryPicture").val(); //store! diary.saveEntry({title:title,body:body,image:img}, function() { pageLoad("main.html"); }); }); });
Note that I've updated my form to include a preview and the entry detail view (not shown here) has been updated to display it.
Ok, so at this point, the core functionality is done. (You can find this version in the www2 folder.) I can add and view content (I didn't bother with edit/delete, but that would be trivial) and I can use the device camera to assign a picture to a diary entry. Now to look into the bug.
My initial thought was to make use of the File API to copy the image to a nice location. Even though Android didn't really need this feature, I thought I'd keep things simple and just use the same logic for all. (To be fair, when I say "all", I really was just testing Android and iOS.)
However, I ran into an interesting issue. When requesting the persistent file system, iOS gave me a directory that was customized for my application:
/var/mobile/Applications/362CC22A-BA60-4D81-876C-21072A06CE16/Documents
Unfortunately, the Android version of the exact same code returned a more generic folder. I could then make a folder for my Android app that was specific to the application itself, but that seemed to be a bit too much work. (Really, it wasn't, I was being lazy.)
I rang up Simon MacDonald, my goto guy for PhoneGap questions (or at least until I annoy him ;) and in discussions with him, I discovered that right now there isn't a simple way, cross platform, to ask the file system for "a safe application-specific place to store my crap." (My words, not his.)
I made a decision at this point. Even though it felt a bit wrong, I decided I'd write code just for iOS. I figured the 'forked' code would be pretty small and therefore it wouldn't "pollute" my code too much. This smells like one of those decisions I may regret later, but for now, it is what I'm going with.
I began by sniffing the device using PhoneGap's Device API.
function deviceready() { console.log('deviceready'); //create a new instance of our Diary and listen for it to complete it's setup diary = new Diary(); if(device.platform === "iOS") { iOS = true; window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(fs) { imgDir = fs.root; diary.setup(startApp); }, null); } else { diary.setup(startApp); } }
Nothing too complex here. If I detect iOS, I request the persistent file system and remember the root directory it gives me.
Next - before I store the Diary entry, if I am on iOS, I simply copy the image over.
if(iOS && img.length > 0) { var fileName = img.split("/").pop(); console.log("fileName="+fileName); window.resolveLocalFileSystemURI(img, function(entry) { entry.moveTo(imgDir, fileName); img = entry.toURL(); //store! diary.saveEntry({title:title,body:body,image:img}, function() { pageLoad("main.html"); }); }, function() { console.log('fail in resolve'); }); } else { //store! diary.saveEntry({title:title,body:body,image:img}, function() { pageLoad("main.html"); }); }
Overall, not as messy as I thought. You can find the complete source code attached below. Enjoy.
Published at DZone with permission of Raymond Camden, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments