This is our final post in our three-part series demonstrating how to use the Salesforce Mobile SDK to build an Android app that works with the Salesforce platform. In our first post, we showed you how to connect to your org. Our second post showed you how to edit and add data to your org from your app. This post will show you how to synchronize data from your Salesforce org to your mobile device and handle scenarios such as network loss. Let’s get right into it!
One of the hardest aspects of mobile development is dealing with data synchronization. How do you handle the situation when you need to add a new broker, but you’re offline? Or what if two agents are updating the same broker—how can you handle the merging of those two changes?
With the Salesforce Mobile SDK, these real-world issues are handled for you by a system called Mobile Sync. Mobile Sync has you map your phone’s local data to the data in Salesforce; it also requires you to define operations for fetching and pushing data—what it calls syncDown
and syncUp
.
To get started with Mobile Sync, create a file in res/raw
called brokerstore.json
:
{
"soups": [
{
"soupName": "brokers",
"indexes": [
{ "path": "Id", "type": "string"},
{ "path": "Name", "type": "string"},
{ "path": "Title__c", "type": "string"},
{ "path": "Phone__c", "type": "string"},
{ "path": "Mobile_Phone__c", "type": "string"},
{ "path": "Email__c", "type": "string"},
{ "path": "Picture__c", "type": "string"},
{ "path": "__local__", "type": "string"},
{ "path": "__locally_created__", "type": "string"},
{ "path": "__locally_updated__", "type": "string"},
{ "path": "__locally_deleted__", "type": "string"},
{ "path": "__sync_id__", "type": "integer"}
]
}
]
}
This file defines the shape of the data on your phone, as well as some additional metadata that’s necessary for the sync.
Next, create a file called brokersync.json
:
{
"syncs": [
{
"syncName": "syncDownBrokers",
"syncType": "syncDown",
"soupName": "brokers",
"target": {"type":"soql", "query":"SELECT Name, Title__c, Phone__c, Mobile_Phone__c, Email__c, Picture__c FROM Broker__c LIMIT 10000"},
"options": {"mergeMode":"OVERWRITE"}
},
{
"syncName": "syncUpBrokers",
"syncType": "syncUp",
"soupName": "brokers",
"target": {"createFieldlist":["Name", "Title__c", "Phone__c", "Mobile_Phone__c", "Email__c", "Picture__c"]},
"options": {"fieldlist":["Id", "Name", "Title__c", "Phone__c", "Mobile_Phone__c", "Email__c", "Picture__c"], "mergeMode":"LEAVE_IF_CHANGED"}
}
]
}
These are the operations that Mobile Sync will use when syncing data down and up.
The code to complete the Mobile Sync process depends on several factors, such as when you want to perform a synchronization, as well as hooking into the Android event cycle when a device loses (and regains) connectivity.
The following code samples will show you a complete example of what needs to happen to get synchronization working, but they should be considered high-level concepts, and not necessarily production-ready enterprise-grade code.
With that said, let’s take a look at how to implement synchronization. First, add this line to the end of our onResume(client: RestClient)
method in MainActivity.kt
:
setupPeriodicSync();
Next, we’ll add a new variable and a new function to the MainActivity
class:
private val SYNC_CONTENT_AUTHORITY =
"com.salesforce.samples.mobilesyncexplorer.sync.brokersyncadapter"
private fun setupPeriodicSync() {
val account = MobileSyncSDKManager.getInstance().userAccountManager.currentAccount
ContentResolver.setSyncAutomatically(account, SYNC_CONTENT_AUTHORITY, true)
ContentResolver.addPeriodicSync(
account, SYNC_CONTENT_AUTHORITY,
Bundle.EMPTY, 10
)
}
Since we’re using ContentResolver
in our function, let’s make sure to import it by adding this line alongside the other import statements near the top of MainActivity.kt
:
import.android.content.ContentResolver
We have defined two methods that trigger synchronization. setupPeriodicSync
will run a sync every 10
seconds. This is much too frequent for a production environment, but we’ll set it this way for demonstration purposes.
We will show the next few code samples all at once, and discuss what they’re doing afterward.
In app/java/com.example.sfdc
, create a new file called BrokerSyncAdapter.kt
and paste these lines into it:
package com.example.sfdc
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.accounts.UserAccountManager
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.example.sfdc.BrokerListLoader
class BrokerSyncAdapter
(
context: Context?, autoInitialize: Boolean,
allowParallelSyncs: Boolean
) :
AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
override fun onPerformSync(
account: Account, extras: Bundle, authority: String,
provider: ContentProviderClient, syncResult: SyncResult
) {
val syncDownOnly = extras.getBoolean(SYNC_DOWN_ONLY, false)
val sdkManager = SalesforceSDKManager.getInstance()
val accManager = sdkManager.userAccountManager
if (sdkManager.isLoggingOut || accManager.authenticatedUsers == null) {
return
}
if (account != null) {
val user = sdkManager.userAccountManager.buildUserAccount(account)
val contactLoader = BrokerListLoader(context, user)
if (syncDownOnly) {
contactLoader.syncDown()
} else {
contactLoader.syncUp() // does a sync up followed by a sync down
}
}
}
companion object {
// Key for extras bundle
const val SYNC_DOWN_ONLY = "syncDownOnly"
}
}
Now, in that same folder, create BrokerListLoader.kt
with these lines:
package com.example.sfdc
import android.content.AsyncTaskLoader
import android.content.Context
import android.content.Intent
import android.util.Log
import com.salesforce.androidsdk.accounts.UserAccount
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.mobilesync.app.MobileSyncSDKManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager
import com.salesforce.androidsdk.mobilesync.manager.SyncManager.MobileSyncException
import com.salesforce.androidsdk.mobilesync.manager.SyncManager.SyncUpdateCallback
import com.salesforce.androidsdk.mobilesync.util.SyncState
import com.salesforce.androidsdk.smartstore.store.QuerySpec
import com.salesforce.androidsdk.smartstore.store.SmartSqlHelper.SmartSqlException
import com.salesforce.androidsdk.smartstore.store.SmartStore
import org.json.JSONArray
import org.json.JSONException
import java.util.ArrayList
class BrokerListLoader(context: Context?, account: UserAccount?) :
AsyncTaskLoader<List<String>?>(context) {
private val smartStore: SmartStore
private val syncMgr: SyncManager
override fun loadInBackground(): List<String>? {
if (!smartStore.hasSoup(BROKER_SOUP)) {
return null
}
val querySpec = QuerySpec.buildAllQuerySpec(
BROKER_SOUP,
"Name", QuerySpec.Order.ascending, LIMIT
)
val results: JSONArray
val brokers: MutableList<String> = ArrayList<String>()
try {
results = smartStore.query(querySpec, 0)
for (i in 0 until results.length()) {
brokers.add(results.getJSONObject(i).getString("Name"))
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException occurred while parsing", e)
} catch (e: SmartSqlException) {
Log.e(TAG, "SmartSqlException occurred while fetching data", e)
}
return brokers
}
@Synchronized
fun syncUp() {
try {
syncMgr.reSync(
SYNC_UP_NAME
) { sync ->
if (SyncState.Status.DONE == sync.status) {
syncDown()
}
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException occurred while parsing", e)
} catch (e: MobileSyncException) {
Log.e(TAG, "MobileSyncException occurred while attempting to sync up", e)
}
}
/**
* Pulls the latest records from the server.
*/
@Synchronized
fun syncDown() {
try {
syncMgr.reSync(
SYNC_DOWN_NAME
) { sync ->
if (SyncState.Status.DONE == sync.status) {
fireLoadCompleteIntent()
}
}
} catch (e: JSONException) {
Log.e(TAG, "JSONException occurred while parsing", e)
} catch (e: MobileSyncException) {
Log.e(TAG, "MobileSyncException occurred while attempting to sync down", e)
}
}
private fun fireLoadCompleteIntent() {
val intent = Intent(LOAD_COMPLETE_INTENT_ACTION)
SalesforceSDKManager.getInstance().appContext.sendBroadcast(intent)
}
companion object {
const val BROKER_SOUP = "brokers"
const val LOAD_COMPLETE_INTENT_ACTION =
"com.salesforce.samples.mobilesyncexplorer.loaders.LIST_LOAD_COMPLETE"
private const val TAG = "BrokerListLoader"
private const val SYNC_DOWN_NAME = "syncDownBrokers"
private const val SYNC_UP_NAME = "syncUpBrokers"
private const val LIMIT = 10000
}
init {
val sdkManager = MobileSyncSDKManager.getInstance()
smartStore = sdkManager.getSmartStore(account)
syncMgr = SyncManager.getInstance(account)
// Setup schema if needed
sdkManager.setupUserStoreFromDefaultConfig()
// Setup syncs if needed
sdkManager.setupUserSyncsFromDefaultConfig()
}
}
What did we just do? Each file has a specific role, and while the objects and fields will certainly be different for your app, the functions and philosophies of these classes are the same:
BrokerListLoader
is responsible for mapping the sync operations you defined in brokersync.json
with the Kotlin code that will actually perform the work. Notice that there are syncUp
and syncDown
methods that use the Mobile SDK’s SyncManager
to load the JSON files and communicate back and forth with Salesforce.BrokerSyncAdapter
can perhaps best be thought of as the code that’s responsible for scheduling the synchronization. That is, it’s the entry point for BrokerListLoader
, and can be called by UI elements (such as during a refresh button click) or Android system events (such as connectivity loss).
Lastly, we need to add one line to our AndroidManifest.xml
file (in app/manifests
). Our new sync functionality will need special Android permissions, which we ask the user to allow upon app installation at runtime. Add the following line with WRITE_SYNC_SETTINGS
to the end of your manifest:
<uses-permission android:name="com.example.sfdc.C2D_MESSAGE" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
</manifest>
Our last step is to test that mobile sync works. With the emulator running, you should be logged in and see a list of brokers. This list should mirror the broker list that you see in your web browser on your local machine. In your web browser, edit one of the broker names, and save that change.
Then, in your emulator, you can power off the phone or switch to another application and then switch back to your sfdc-mobile-app
. You should see your list of broker names updated with the change you made in your browser:
And that’s all! Again, this is just a foundational building block for you to learn from. In just about 150 lines of code, we’ve devised a solution for a rather complicated series of moving parts:
The Salesforce Mobile SDK makes it tremendously easy to connect mobile devices with Salesforce data. In this post, you learned how to query and manipulate data from your phone, and saw the results reflected instantaneously on Salesforce. You also learned about Mobile Sync and its role in anticipating issues with connectivity.
And yet, with all of this information, there’s still so much more that the Salesforce Mobile SDK can do. Take a look at the complete documentation on working with the SDK. Or, if you prefer to do more coding, there are plenty of Trailhead tutorials to keep you busy. We can’t wait to see what you build!