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.
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.
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:
//youtu.be/fG_OvWdfgVc?t=1h2m38s
[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!