visit
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!
With the Salesforce Mobile SDK, these real-world issues are handled for you by a system called . 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"}
]
}
]
}
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"}
}
]
}
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.
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()
}
}
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>
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 yet, with all of this information, there’s still so much more that the Salesforce Mobile SDK can do. Take a look at the on working with the SDK. Or, if you prefer to do more coding, there are to keep you busy. We can’t wait to see what you build!