Open Source Software Technical Articles

Want the Best of the Wazi Blogs Delivered Directly to your Inbox?

Subscribe to Wazi by Email

Your email:

Connect with Us!

Current Articles | RSS Feed RSS Feed

Build an app to capture photos using Apache Cordova and jQuery Mobile

  
  
  

Apache Cordova is a platform for building native mobile applications using common web technologies, including HTML, CSS, and JavaScript. It offers a set of APIs that allow application developers to access objects such as audio, camera, and filesystem on mobile devices using JavaScript. Meanwhile, jQuery Mobile, one of the best mobile web application frameworks, allows developers to create web applications that are mobile-friendly. Here's how you can use Apache Cordova with jQuery Mobile to create a native Android application that can capture camera photos or get photos from the gallery and save them on a device's SD card.

In a previous article, I talked about how to build an application using Apache Cordova and jQuery Mobile that records audio, saves the recordings, and lets you manage your saved audio memos from a list. Let's enhance that application to let users save visual memos in the same application. At the same time, we can also enhance the user interface and optimize navigation between pages. Let's see how.

We can change the look and feel of the application by using the data-theme attribute. Let's look at the newly added code of the home view (memoList):

 <div data-role="page" id="memoList" data-theme="b">
     <div data-role="header">    
        <a href="#about" data-role="button" data-icon="info">About</a>           
        <h1>Memo List</h1>
        <a id="newMemo" data-role="button" data-icon="plus">New</a>                     
     </div>
     <div data-role="content">
         <ul data-role="listview" id="memoListView">
         </ul> 
        <div data-role="popup" id="memoTypeSelection" data-dismissible="false">
            <ul data-role="listview" data-inset="true" style="min-width:210px;" data-theme="b">
               <li data-role="divider" data-theme="a">Memo Type</li>
               <li><a href="#memoCapture?newMemo=voice">Voice Memo</a></li>
               <li><a href="#memoCapture?newMemo=photo">Photo Memo</a></li>
            </ul>
         </div>                   
     </div>
    <!-- ... -->              
  </div>

When users click on the New button, they can create either a voice or a photo memo:

Adding a new memo

We let users select which type of memo to create with a popup selection menu called memoTypeSelection that contains a child list view with the selection options. When a user clicks any of the selection items, the code sends the parameter newMemo to the memoCapture view, which handles the creation of both audible and visual memos, with a value of either "voice" or "photo."

The next code snippet shows how to display the selection popup when users tap the newMemo button. This code is part of memoList.js, which represents the view controller of the memoList view. You can learn more about the view controller role from my previous article.

(function() {
    $(document).on("pageinit", "#memoList", function(e) {
        $("#newMemo").on("tap", function(e) {
            e.preventDefault();
            $("#memoTypeSelection").popup("open");
        });         
        
    });
    // Some Code here omitted for simplicity
})();

The next snippet handles the memoCapture view, which creates and updates both voice and photo memos.

<div data-role="page" id="memoCapture" data-theme="b">
     <div data-role="header">
         <a href="#memoList" data-role="button" data-icon="home">Home</a>                                
         <h1>Your Memo</h1>                                  
         <a href="#" data-role="button" data-rel="back" data-icon="back">Back</a> 
      </div>
      <div data-role="content">
        <div data-role="fieldcontain">
            <input type="hidden" id="mid"/>
            <input type="hidden" id="mtype"/>
                            
            <label for="title">Title</label> 
            <input type="text" name="title" id="title"></input><br/>
                
            <label for="desc">Description</label> 
            <textarea name="desc" id="desc"></textarea><br/>    
            
            <label for="mtime">Time</label> 
            <input type="text" name="mtime" id="mtime" readonly="readonly"></input><br/>   
            
            <input type="hidden" id="location"/>
            
            <div class="center-wrapper">
                <input type="button" id="getPhoto" data-icon="camera" value="Get Photo" class="center-button" data-inline="true"/>
                <input type="button" id="recordVoice" data-icon="audio" value="Record" class="center-button" data-inline="true"/>
                <input type="button" id="playVoice" data-icon="refresh" value="Playback" class="center-button" data-inline="true"/><br/>
            </div>
                                
            <input type="button" value="Save Memo" data-icon="check" id="saveMemo"/> 
            <input type="button" id="removeMemo" data-icon="delete" value="Remove"/> <br/>                 
            
            <div style="width: 100%;">
                <img id="imageView" style="width: 100%;"></img>
            </div><br/>   
            
            <div data-role="popup" id="photoTypeSelection" data-dismissible="false">
               <ul data-role="listview" data-inset="true" style="min-width:210px;" data-theme="b">
                  <li data-role="divider" data-theme="a">Get Photo From</li>
                  <li><a id="photoFromGallery" href="#">Gallery</a></li>
                  <li><a id="photoFromCamera" href="#">Camera</a></li>
               </ul>
            </div>                                           
        </div>
      </div>
</div>

The memoCapture view has the following elements:

  • mid – hidden field that contains the ID of the memo.
  • mtype – hidden field that contains the type of the memo ("voice" or "photo").
  • location – hidden field that contains the location of the memo.
  • title, description, and mtime – text fields that represent the title, description, and time of the memo.
  • center-wrapper – div that contains several buttons: getPhoto is displayed if the memo type is "photo"; recordVoice is displayed if the memo type is "voice"; playVoice is displayed if the memo type is "voice" and a voice is already recorded.
  • saveMemo – button to save the memo.
  • removeMemo – button to remove the memo. It is displayed in edit mode.
  • imageView – image, used to display a photo memo.
  • photoTypeSelection – selection popup that appears when a user clicks the getPhoto button. This selection popup contains two options that let users pick a photo from the gallery or capture a photo using thecamera.

When they click on the photo memo option, for instance, users go to the photo memo page, on which they can enter a photo title and description and get a photo from either the device camera or the gallery:

Photo memo option

The next code snippet shows the tapping event handlers of the memoCapture elements. This code is part of memoCapture.js, which represents the view controller for the memoCapture view.

(function() {
    var memoManager = MemoManager.getInstance();
    $(document).on("pageinit", "#memoCapture", function(e) {
        e.preventDefault();
        
        $("#saveMemo").on("tap", function(e) {
            e.preventDefault();
            var memoItem = new MemoItem({
                                 "type": $("#mtype").val(),
                                 "title": $("#title").val() || $("#mtime").val() , 
                                 "desc": $("#desc").val() || "", 
                                 "location": $("#location").val() || "",
                                 "mtime": $("#mtime").val(),
                                 "id": $("#mid").val() || null
                                 });

            memoManager.saveMemo(memoItem);
            
            $.mobile.changePage("#memoList");
        });        
        
        $("#removeMemo").on("tap", function(e) {
            e.preventDefault();
            AppUtil.showConfirmationMessage("Are you sure you want to remove this memo?", removeCurrentMemo);
        });         
        
        $("#recordVoice").on("tap", function(e) {
            e.preventDefault();
        
            var recordingCallback = {};
            
            recordingCallback.recordSuccess = handleRecordSuccess;
            recordingCallback.recordError = handleRecordError;
            
            memoManager.recordVoice(recordingCallback);
         }); 
        
        $("#playVoice").on("tap", function(e) {
            e.preventDefault();
        
            var playCallback = {};
            
            playCallback.playSuccess = handlePlaySuccess;
            playCallback.playError = handlePlayError;
            
            memoManager.playVoice($("#location").val(), playCallback);
        });   
        
        $("#getPhoto").on("tap", function(e) {
            e.preventDefault();
            
            $("#photoTypeSelection").popup("open");
        });        
        
        $("#photoFromGallery").on("tap", function(e) {
            e.preventDefault();
            $("#photoTypeSelection").popup("close");
            
            getPhoto(true);
         });    
        
        $("#photoFromCamera").on("tap", function(e) {
            e.preventDefault();
            $("#photoTypeSelection").popup("close");
            
            getPhoto(false);
         });     
    });
    
    function getPhoto(fromGallery) {
        var capturingCallback = {};
        
        capturingCallback.captureSuccess = handleCaptureSuccess;
        capturingCallback.captureError = handleCaptureError;
        
        memoManager.getPhoto(capturingCallback, fromGallery);        
    }
    
    function removeCurrentMemo() {
        memoManager.removeMemo($("#mid").val());
        $.mobile.changePage("#memoList");        
    }

    function handleRecordSuccess(newFilePath) {            
        var currentFilePath = newFilePath;
        
        $("#location").val(currentFilePath);    
        $("#playVoice").closest('.ui-btn').show();  
    }
        
    function handleRecordError(error) {
        displayMediaError(error);
    }  
    
    function handlePlaySuccess() {
        console.log("Voice file is played successfully ...");
    }
    
    function handlePlayError(error) {
        displayMediaError(error);
    }        
    
    function handleCaptureSuccess(newFilePath) {            
        var currentFilePath = newFilePath;
        
        $("#imageView").show();  
        $("#imageView").attr("src", currentFilePath);         
        $("#location").val(currentFilePath);  
    }    
    
    function handleCaptureError(message) {
        console.log("Camera capture error");
    }      
        
    function displayMediaError(error) {
        if (error.code == MediaError.MEDIA_ERR_ABORTED) {
            console.log("Media aborted error");
        } else if (error.code == MediaError.MEDIA_ERR_NETWORK) {
            console.log("Network error");
        } else if (error.code == MediaError.MEDIA_ERR_DECODE) {
            console.log("Decode error");
        } else if (error.code ==  MediaError.MEDIA_ERR_NONE_SUPPORTED) {
            console.log("Media is not supported error");
        } else {
            console.log("General Error: code = " + error.code);
        }        
    }
    // Code here is omitted for simplicity
})();

The tapping handlers of the form elements do the following:

  • When a user taps saveMemo, the handler creates a new MemoItem object and populates it with the memo data. It saves the memo by calling the saveMemo() method of MemoManager, and directs the user to the memos listing view to see the saved memo.
  • When a user taps removeMemo, the handler displays a confirmation message. If the user confirms, then handler removes the memo by calling the removeMemo() method of MemoManager, passing the memo ID to the method.
  • When a user taps recordVoice or playVoice, the handler calls the recordVoice() or playVoice() method of MemoManager.
  • When a user taps getPhoto, the handler opens the photo type selection popup menu to allow the user to select whether to get a photo from the camera or the gallery.
  • When a user taps photoFromGallery or photoFromCamera, the handler closes the selection popup menu and then calls the getPhoto() method of MemoManager to get a photo from the camera or the gallery. The getPhoto() method of MemoManager takes two parameters: a capturingCallback object, which includes the captureSuccess or captureError property, depending on the success or failure of the operation, and a boolean that indicates whether the image is to be picked from the gallery or captured from the camera.

For example, when users click on the voice memo option, they get the voice memo page:

A new voice memo

memoManager calls handleRecordSuccess(filePath) when voice recording succeeds, and displays the playVoice button to allow users to play the recorded voice. It calls handleCaptureSuccess(filePath) when the user captures a photo from the camera or picks one from gallery successfully, and the displays the imageView image element with the captured photo. Both handlers update the hidden location field with the location of the voice or photo item.

The memoList view can call the memoCapture view in either photo or voice mode, which is why we need to show and hide elements based on the current view mode. As I mentioned earlier, we specify memoCapture mode in the newMemo parameter that is passed from the memoList view.

The next code snippet shows how the memoCapture view captures this parameter and updates the form elements.

(function() {
    
    // ...
    
    $(document).on("pageshow", "#memoCapture", function(e) {
        e.preventDefault();
        
        var memoID = ($.mobile.pageData && $.mobile.pageData.memoID) ? $.mobile.pageData.memoID : null;
        var memoType = ($.mobile.pageData && $.mobile.pageData.newMemo) ? $.mobile.pageData.newMemo : null;        
        var memoItem = null;
        var isNew = true;
        
        if (memoID) {
            
            //Update Memo
            memoItem = memoManager.getMemoDetails(memoID);            
            isNew = false;
        } else {
            
            //Create a new Memo
            memoItem = new MemoItem({"type": memoType});        
        }
        
        populateRecordingFields(memoItem, isNew);
    });
        
    function populateRecordingFields(memoItem, isNew) {
        $("#mid").val(memoItem.id);
        $("#mtype").val(memoItem.type);
        $("#title").val(memoItem.title);
        $("#desc").val(memoItem.desc);
        $("#location").val(memoItem.location);
        $("#mtime").val(memoItem.mtime);
        
        $("#recordVoice").closest('.ui-btn').hide();
        $("#getPhoto").closest('.ui-btn').hide();            
        $("#playVoice").closest('.ui-btn').hide();        
        $("#removeMemo").closest('.ui-btn').hide(); 
        $("#imageView").hide();    
        $("#imageView").attr("src", "");   
        
        if (! isNew) {
            $("#removeMemo").closest('.ui-btn').show(); 
        }
        
        if (memoItem.type == "voice") {
            $("#recordVoice").closest('.ui-btn').show();  
            
            if (memoItem.location && memoItem.location.length > 0) {
                $("#playVoice").closest('.ui-btn').show();
            }            
        } else if (memoItem.type == "photo") {
            $("#getPhoto").closest('.ui-btn').show();
            
            if (memoItem.location && memoItem.location.length > 0) {
                $("#imageView").show();  
                $("#imageView").attr("src", memoItem.location); 
            }          
        }
    }
    
    //Code is omitted here for simplicity

})();

In the pageshow event of the memoCapture view, the jQM page params plugin retrieves memoID and memoType, which I described in detail in the previous article. If a memoID is passed, it means that the memo is in edit mode, and the isNew attribute will be set to false. In edit mode, the memoCapture view controller uses memoID to retrieve the memo details by calling the getMemoDetails() method of MemoManager. If there is no memoID, then this is a new memo, and memoCapture uses memoType to create a new MemoItem object.

In populateRecordingFields, memoCapture displays the removeMemo button if the memo is in edit mode. If the memo type is "voice" then it shows the recordVoice button, while if the memo type is "photo" it shows the getPhoto button.

If the memo is in edit mode and there is already a saved photo or voice memo, memoCapture shows the playVoice button for "voice" memo types and imageView for "photo"

Here's the photo details view:

Photo memo

Providing a visual cue

When we display the memo list, we can customize the view and display a voice icon for the voice items and a camera icon for the photo items:

(function() {
    var memoManager = MemoManager.getInstance();
    $(document).on("pageshow", "#memoList", function(e) {
        e.preventDefault();
        
        updatememoList();
    });

    function updatememoList() {
        var memos = memoManager.getMemos();
        $("#memoListView").empty();
                
        if (jQuery.isEmptyObject(memos)) {
            $("<li>No Memos Available</li>").appendTo("#memoListView");
        } else {
            for (var memo in memos) {
                var type = "";
                
                if (memos[memo].type == "voice") {
                    type = "audio";
                } else if (memos[memo].type == "photo") {
                    type = "camera";                    
                }
                
                $("<li data-icon='" + type + "'><a href='#memoCapture?memoID=" + memos[memo].id + "'>" + 
                        memos[memo].title + "</a></li>").appendTo("#memoListView");
            }
        }
        
        $("#memoListView").listview('refresh');        
    }
})();

In the pageshow event of the memoList view, the memoList view controller calls updateMemoList(), which retrieves the memos list using the getMemos() method of MemoManager. If the memo type is "voice," it uses the "audio" icon as the data-icon of the item; if the memo type is "photo," it uses the "camera" icon.

On the home screen of the application, the listing page now displays the audio and photo items in a single unified listing:

memoList view

Now that we've finished all the view controller logic to capture and display memos, let's see how the getPhoto() method of MemoManager works using Apache Cordova APIs:

var MemoManager = (function () {     
  var instance;
 
  function createObject() {    
          
          // Code is omitted here for simplicity    
          
          getPhoto: function (capturingCallback, fromGallery) {      
              var source = Camera.PictureSourceType.CAMERA;
              
              if (fromGallery) {
                  source = Camera.PictureSourceType.PHOTOLIBRARY;  
              }
              
              navigator.camera.getPicture(capturingCallback.captureSuccess, 
                                            capturingCallback.captureError, 
                                            { 
                                             quality: 30, 
                                             destinationType: Camera.DestinationType.FILE_URI, 
                                             sourceType: source,
                                             correctOrientation: true 
                                            });              
          }
          
          // Code is omitted here for simplicity
    };
  };
 
  return {
    getInstance: function () {
      if (!instance) {
          instance = createObject();
      }
 
      return instance;
    }
  }; 
})();

getPhoto() calls navigator.camera.getPicture(), specifying success callback capturingCallback.captureSuccess when the operation succeeds and error callback capturingCallback.captureError when the operation fails. The last parameter of navigator.camera.getPicture() represents a configuration object, for which we can specify the following attributes:

  • quality represents the quality of the output image, and can have a value from 0 to 100. We specified 30.
  • destinationTyperepresents the type of the result. It can have one of two values:
    • DATA_URL means that the output result will be returned as a base64-encoded string.
    • FILE_URI means that the output result will be returned as a URI to the actual path of the image on the device.
  • sourceTyperepresents the source of the picture. It can have one of two values:
    • PHOTOLIBRARY means that the source of the image is the device photo library.
    • CAMERA means that the source of the image is the device camera.
  • correctOrientation – if set to true, Cordova will handle correcting the image orientation.

To see how Cordova implements voice capture and playback logic, see the previous article.

Finally, here's a tip for when you're using jQuery Mobile with Apache Cordova and you find the transitions between pages of your mobile application are too slow. I had this issue with one of my Cordova applications and jQuery Mobile 1.4. After spending some time investigating the problem, I found that the solution is to disable jQuery Mobile transition effects as follows:

$.mobile.defaultPageTransition   = 'none';
$.mobile.defaultDialogTransition = 'none';

Disabling transition effects dramatically boosts your Cordova application transition performance.

You can download my complete application code and study it at your leisure. It implements a real Android mobile application that is currently published on both the Opera Android Store and Google Play Store – go ahead, try it! Then you can start developing your own native mobile applications using Apache Cordova and jQuery Mobile and publish them yourself on the app stores.


Do you want to receive a compilation of Wazi's top
blog posts in the past year delivered directly to your inbox?






This work is licensed under a Creative Commons Attribution 3.0 Unported License
Creative Commons License.

Comments

Regards, 
 
Thank you very much for sharing. 
 
A minimum observation: In the source code ("memo.zip"), there is a file "www/js/index.js" not used. 
 
Alex
Posted @ Wednesday, March 26, 2014 3:47 PM by Alex
Thank you indeed for sharing.  
The app is working fine for taking pictures, on both iOS and Android device, but the voicerecording does not work on my Android device. I'm having a MediaError.MEDIA_ERR_DECODE error.  
I'm having the same problem in your previous VoiceMemo project, also on Android.
Posted @ Tuesday, April 29, 2014 6:35 AM by Gerard
I think this is among the most vital information for me. And i'm glad reading your article. But want to remark on some general things, The website style is wonderful, the articles is really excellent.
Posted @ Sunday, July 13, 2014 6:39 AM by Available Photo Booths in Louisiana
Post Comment
Name
 *
Email
 *
Website (optional)
Comment
 *

Allowed tags: <a> link, <b> bold, <i> italics