Since the release of the Salesforce Mobile Packs a few weeks ago, we’ve been building out from our initial set of jQuery Mobile, AngularJS and Backbone.js apps for Visualforce, Node.js and PHP. If you’ve been watching the Backbone Mobile Pack on GitHub, you’ll have noticed the recent addition of a couple of XCode projects in the samples directory: BackboneHybrid and BackboneCapture.

BackboneHybrid

The BackboneHybrid sample converts the existing Contact browser sample to a hybrid app running on the Salesforce Mobile SDK (which itself is built on Cordova, aka PhoneGap). It’s interesting to look at the changes required to ‘hybridize’ the sample – they are pretty much restricted to app startup, in particular, obtaining an OAuth access token. The critical code is registered with the Mobile SDK as an event listener for the salesforceSessionRefresh event:

// When this function is called, Cordova has been initialized and is ready to roll 
function onDeviceReady() {
  //Call getAuthCredentials to get the initial session credentials
  cordova.require("salesforce/plugin/oauth").getAuthCredentials(salesforceSessionRefreshed, 
    getAuthCredentialsError);

  //register to receive notifications when autoRefreshOnForeground refreshes the 
  // sfdc session
  document.addEventListener("salesforceSessionRefresh", salesforceSessionRefreshed,
    false);
}

// This function is called when the OAuth flow is complete
function salesforceSessionRefreshed(creds) {
  // Depending on how we come into this method, `creds` may be callback data from 
  // the auth plugin, or an event fired from the plugin.  The data is different 
  // between the two.
  var credsData = creds;
  if (creds.data)  // Event sets the `data` object with the auth data.
    credsData = creds.data;

  // Create a ForceTK client object - this holds the OAuth access token, 
  // instance URL etc
  var client = new forcetk.Client(credsData.clientId, credsData.loginUrl);
  client.setSessionToken(credsData.accessToken, apiVersion, credsData.instanceUrl);
  client.setRefreshToken(credsData.refreshToken);
  myapp(client);
}

The myapp() function behaves identically to the web-based apps, initializing Backbone.Force with the ForceTK client, creating a Model and Collection for the Contact standard object, setting up Views and a Router, and starting the Backbone app (see the blog entry on the Backbone Mobile Pack for MUCH more detail!). This is a real benefit of using a framework like Backbone.Force built on ForceTK – you can reuse the same application code in many contexts – Visualforce, off-platform web apps, and hybrid mobile apps.

BackboneCapture

So, you may be asking yourself, “What’s the point of a contact browser as a hybrid app – it just does the same as the web app!”. If so, you have a point – BackboneHybrid is really just to show the code required to get started with Backbone and the Mobile SDK. How about something more interesting – how about capturing photos, audio and video from the phone, uploading the media file to Chatter Files, and posting a Chatter message on a Contact page. That’s exactly what BackboneCapture does; here it is in action at last week’s Ottawa Salesforce Developer User Group meeting:

[The relevant section is at 01:02:38 - you may need to skip to that point if the video doesn't start in the right place].

Let’s take things step by step. First, how do we capture media on the device? Well, Cordova offers a set of APIs for the purpose. For simplicity and brevity, we’ll look at the code to capture a photo here; capturing audio and video works similarly, but is a little more involved – see the BackboneCapture source for details.

To capture a photo, we call navigator.camera.getPicture(), passing it success and failure callbacks and an options object. Here’s the handler for the ‘Get Picture’ button showing the call:

getPicture: function(){
  self = this;
  var options = {
    quality: 50,
    correctOrientation: true,
    sourceType: Camera.PictureSourceType.CAMERA,
    destinationType: Camera.DestinationType.DATA_URL
  };
  navigator.camera.getPicture(function(imageData) {
    uploadContent('image.png', 'image', imageData, self.model.attributes.Id);
  }, function(errorMsg) {
    // Most likely error is user cancelling out
    alert("Error: "+errorMsg);
  }, options);
  return false;
},

As you can see, we set up the options object for 50% quality (trading off image quality for a smaller file size), capturing an image from the camera as a data URL – a Base64 encoding of the image data.

On taking a photo and clicking the ‘use’ button, the uploadContent() callback will fire. Here’s take a closer look:

var uploadContent = function(filename, contentType, imageData, contactId){
  // Create a new ContentVersion and save it
  // Note that this mechanism is limited to 50 MB of data - to upload
  // bigger files we would need send binary data in a multipart message
  // Note - 50MB is about 6 minutes of video
  var contentVersion = new app.ContentVersion({
    Origin: 'H',            // 'H' for a Chatter File, 'C' for a Content document
    PathOnClient: filename, // Hint as to type of data
    VersionData: imageData  // Base64 encoded file data
  });
  $.mobile.loading( 'show', {
    text: 'Uploading '+contentType+' to Chatter Files',
    textVisible: true,
  });
  contentVersion.save(null, {
    success: function() {
      // Fetch the ContentVersion record to get the ContentDocumentId
      $.mobile.loading( 'show', {
        text: 'Fetching Document ID',
        textVisible: true,
      });
      contentVersion.fetch({
        success: function() {
          // Post to Chatter feed for Contact
          $.mobile.loading( 'show', {
            text: 'Posting Chatter message',
            textVisible: true,
          });
          var payload = { attachment: {
              attachmentType: "ExistingContent",
              contentDocumentId: contentVersion.attributes.ContentDocumentId
            },
            body: {
              messageSegments : [{
                type: 'Text',
                text: 'Here is my '+contentType
              }]
            }
          };
          client.ajax('/v27.0/chatter/feeds/record/'+contactId+'/feed-items',
            function(){
              $.mobile.loading( "hide" );
              alert('Posted '+contentType+' successfully');
            },
            showError,
            'POST',
            JSON.stringify(payload));
        },
        error: showError
      });
    },
    error: function(model, xhr, options) {
      showError('Error: status='+xhr.status+
        '\nstatusText='+xhr.statusText+
        '\nresponseText='+xhr.responseText);
    }
  });                           
};

There’s quite a bit of code there – let’s break it down…

First we create a ContentVersion instance. Earlier in the app, we defined a ContentVersion Model:

app.ContentVersion = Backbone.Force.Model.extend({
  type:'ContentVersion',
  fields:['ContentDocumentId']
});

This is very similar to the Contact Model – we just specify a different type and set of fields. ContentDocumentId is all we need when we fetch a ContentVersion record.

Now, back in uploadContent(), we create a ContentVersion instance:

var contentVersion = new app.ContentVersion({
  Origin: 'H',            // 'H' for a Chatter File, 'C' for a Content document
  PathOnClient: filename, // Hint as to type of data
  VersionData: imageData  // Base64 encoded file data
});

Luckily, ContentVersion needs the file data in Base64 format, and that’s exactly what getPicture() gives us. Now (skipping over the code to show the jQuery Mobile ‘spinner’) we can save the ContentVersion to Force.com:

contentVersion.save(null, { //... success, error handlers

To post the file to a Chatter feed, we actually need the ContentDocumentId for the Chatter File, so we fetch the new ContentVersion to discover this:

contentVersion.fetch({ //... success, error handlers

ContentDocumentId in hand, we can create the payload for a Chatter post:

var payload = { attachment: {
    attachmentType: "ExistingContent",
    contentDocumentId: contentVersion.attributes.ContentDocumentId
  },
  body: {
    messageSegments : [{
      type: 'Text',
      text: 'Here is my '+contentType
    }]
  }
};

And post it to the contact’s Chatter feed:

client.ajax('/v27.0/chatter/feeds/record/'+contactId+'/feed-items',
  function(){
    $.mobile.loading( "hide" );
    alert('Posted '+contentType+' successfully');
  },
  showError,
  'POST',
  JSON.stringify(payload));

We use the low-level client.ajax() method here to access the Chatter REST API, since posting to a Chatter feed doesn’t really map into Backbone’s Model/Controller pattern.

As this sample shows, it’s straightforward to use the Backbone Mobile Pack with any standard object (in fact, any object, standard or custom) in Force.com, access device functionality via the Cordova APIs, and integrate with any Force.com REST-based API via ForceTK.

What functionality are you looking to build in your mobile apps? Let us know what kind of sample code you’d like to see next!

tagged , , Bookmark the permalink. Trackbacks are closed, but you can post a comment.
  • Silky’s New Beginning

    Thanks for the info :)