Abstract
Mobile apps that rely on backend servers for their data needs should provide seamless offline capability. To provide this capability, apps must implement a data sync mechanism that takes connection availability, authentication, and battery usage, among other things, in to account. In Part 1, we discussed how to leverage the Android sync adapter framework to implement these features in a sample restaurant app, mainly using content provider. In this part we will explain the remaining pieces, the sync adapter and authenticator. We will also look at how to use Google cloud messaging (GCM) notifications to trigger the data sync with a backend server.
Contents
Abstract
Overview
Data Sync Strategy for Restaurant Sample App – Little Chef
Sync Adapter Implementation
Authenticator Implementation
Configuring and Triggering the Sync
About the Author
Overview
If you haven’t already read Part 1, please refer to the following link:
Part 1 covers the integration of content provider with our sample app, which uses local SQLite database.
Though the content provider is optional for sync adapter, it abstracts the data model from other parts of the app and provides a well-defined API for integrating with other components of Android framework (for example, loaders).
To fully integrate Android sync adapter framework into our sample app, we need to implement the following pieces: sync adapter, a sync service that links the sync adapter with Android sync framework, authenticator, and an authenticator service to bridge the sync adapter framework and authenticator.
For the authenticator we will use a dummy account for demo purposes.
Data Sync Strategy for Restaurant Sample App – Little Chef
As we discussed in previous articles, “Little Chef” is a sample restaurant app (Figure 1) with several features including menu content, loyalty club, and location-based services among others. The app uses a backend server REST API to get the latest menu content and updates. The backend database can be updated using a web frontend. The server can then send GCM notifications for data sync as required.
Figure 1: A Restaurant Sample App - Little Chef
When the restaurant manager updates menu items on the backend server, we need an efficient sync strategy for propagating these changes to all the deployed mobile devices/apps.
Sync adapter framework has several ways to accomplish this—at regular intervals, on demand—when the network becomes available. If the app mainly relies on data coming from server, we can use GCM notifications to inform all the clients to sync. This is more efficient and reduces unnecessary sync requests, saving battery and other resource usage. This is the approach taken in Little Chef sample app. For details on other sync strategies please refer https://developer.android.com/training/sync-adapters/running-sync-adapter.html
We also use a simple database version tagging to determine if the local SQLite data model is out of sync with the backend data model. For every change made on the backend server, the sever DB version tag is increased. When we receive a sync request, we compare the local DB version and the remote DB version, and only if they differ do we proceed with the sync. As the sync adapter implementation just relies on REST API end-points, it is agnostic to any server-side implementation specifics.
Ideally, the server and client need to keep track of all the DB records that have changed and replay those changes on the client side. As our sample app data model is small, the actual sync is going to replace the local data with the latest copy from server (but only when the DB versions differ).
Sync Adapter Implementation
We implement the Sync Adapter by extending the AbstractThreadedSyncAdapter class, the main method to focus on is onPerformSync. The actual sync logic resides here. The Sync Adapter framework by itself does not provide any data transfer, connection, or conflict resolution, it just calls this method whenever a sync is triggered. It does run the Sync Adapter in a background thread, so at least we do not have to worry about launch issues.
In the code snippet below, onPerformSync uses the Retrofit* REST client library to get the latest server DB version. It compares it with local DB version and determines if a sync is required. If a sync is required, it will do another REST call to download all the menu items data from the server and replace the local content with the one from server.
Content Providers come in handy here. We can issue a “notify” to all Content Provider listeners. As the sample app uses Loaders and CursorAdapter to display the Menu items, they automatically get refreshed with new values immediately after the sync.
public class RestaurantSyncAdapter extends AbstractThreadedSyncAdapter { private static final String TAG = "RestaurantSyncAdapter"; private SharedPreferences sPreferences; private RestaurantRestService restaurantRestService; private ContentResolver contentResolver; private void init(Context c) { sPreferences = PreferenceManager.getDefaultSharedPreferences(c); RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint("http://my.server.com/") .build(); restaurantRestService = restAdapter.create(RestaurantRestService.class); contentResolver = c.getContentResolver(); } public RestaurantSyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); init(context); } public RestaurantSyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); init(context); } private ContentValues menuToContentValues(RestaurantRestService.RestMenuItem menuItem) { ContentValues contentValues = new ContentValues(); contentValues.put("_id", menuItem._id); contentValues.put("category", menuItem.category); contentValues.put("description", menuItem.description); contentValues.put("imagename", menuItem.imagename); contentValues.put("menuid", menuItem.menuid); contentValues.put("name", menuItem.name); contentValues.put("nutrition", menuItem.nutrition); contentValues.put("price", menuItem.price); return contentValues; } @Override public void onPerformSync(Account account, Bundle bundle, String s, ContentProviderClient contentProviderClient, SyncResult syncResult) { try { // Check if any DB changes on server int serverDBVersion = restaurantRestService.dbVersion().user_version; int localDBVersion = sPreferences.getInt("DB_VERSION", 0); Log.d(TAG, "onPerformSync: localDBversion " + Integer.toString(localDBVersion) + " serverDBVersion " + Integer.toString(serverDBVersion)); if (serverDBVersion > 0 && serverDBVersion != localDBVersion) { // fetch menu items from server and update the local DB List<ContentValues> contentValList = new ArrayList<>(); for (RestaurantRestService.RestMenuItem menuItem: restaurantRestService.menuItems()) { ContentValues contentValues = menuToContentValues(menuItem); contentValues.putNull("_id"); contentValList.add(contentValues); } int deletedRows = contentProviderClient.delete(RestaurantContentProvider.MENU_URI,null,null); int insertedRows = contentProviderClient.bulkInsert(RestaurantContentProvider.MENU_URI, contentValList.toArray(new ContentValues[contentValList.size()])); Log.d(TAG, "completed sync: deleted " + Integer.toString(deletedRows) + " inserted " + Integer.toString(insertedRows)); // update local db version sPreferences.edit().putInt("DB_VERSION", serverDBVersion).commit(); // notify content provider listeners contentResolver.notifyChange(RestaurantContentProvider.MENU_URI, null); } } catch (Exception e) { Log.d(TAG, "Exception in sync", e); syncResult.hasHardError(); } } }
The Sync Adapter gets instantiated via its corresponding Sync Service. Implementing Sync Service is straightforward, just instantiate the Sync Adapter object in the OnCreate method of the Sync Service and return its Binder object in the onBind method call. Please refer to next code snippet.
public class RestaurantSyncService extends Service { private static final String TAG = "RestaurantSyncService"; private static final Object sAdapterLock = new Object(); private static RestaurantSyncAdapter sAdapter = null; @Override public void onCreate() { super.onCreate(); Log.e(TAG, "onCreate()"); synchronized (sAdapterLock) { if (sAdapter == null) { sAdapter = new RestaurantSyncAdapter(getApplicationContext(), true); } } } @Override public IBinder onBind(Intent intent) { return sAdapter.getSyncAdapterBinder(); } }
We only have two more items to complete the Sync Adapter implementation: creating an xml file describing the Sync Adapter configuration (metadata), and, like any Android Service, adding our Sync Service to Android Manifest entry.
We can give any name to config xml file (for example, syncadapter.xml) and place it in res/xml folder.
<?xml version="1.0" encoding="utf-8"?><sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:contentAuthority="com.example.restaurant.provider" android:accountType="com.example.restaurant" android:userVisible="false" android:supportsUploading="false" android:allowParallelSyncs="false" android:isAlwaysSyncable="true"/>
For detailed explanation of each field, please refer to https://developer.android.com/training/sync-adapters/creating-sync-adapter.html#CreateSyncAdapterMetadata
Please note in Code Snippet 3, we use “com.example.restaurant” as the accountType. We will use this when implementing the Authenticator.
And the Android Manifest entry for Sync Service is shown below. We refer to the above Sync Adapter xml in android:resource under the meta-data entry.
<service android:name=".RestaurantSyncService" android:enabled="true" android:exported="true" android:process=":sync"><intent-filter><action android:name="android.content.SyncAdapter" /></intent-filter><meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" /></service>
Please use the official documentation for detailed reference: https://developer.android.com/training/sync-adapters/creating-sync-adapter.html
Authenticator Implementation
Android Sync Adapter framework requires an Authenticator to be part of the implementation. This can be very useful if we need to implement backend authentication. We can then leverage Android Accounts API for seamless integration.
For Sync Adapter framework to work, we need an account, even a dummy account works. In this case, we can use the default stub implementation for Authenticator component. This makes our Authenticator implementation a lot easier.
Similar to Sync Adapter implementation, we first create an Authenticator class and an Authenticator Service to go with it, then we create a metadata xml for Authenticator, and of course the Android Manifest entry for Authenticator Service.
We implement Authenticator by extending the AbstractAccountAuthenticator class. Use your favorite IDE to generate default/stub method implementations.
public class Authenticator extends AbstractAccountAuthenticator { // Simple constructor public Authenticator(Context context) { super(context); } @Override public Bundle editProperties(AccountAuthenticatorResponse accountAuthenticatorResponse, String s) { throw new UnsupportedOperationException(); } @Override public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s2, String[] strings, Bundle bundle) throws NetworkErrorException { return null; } @Override public Bundle confirmCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, Bundle bundle) throws NetworkErrorException { return null; } @Override public Bundle getAuthToken(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public String getAuthTokenLabel(String s) { throw new UnsupportedOperationException(); } @Override public Bundle updateCredentials(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String s, Bundle bundle) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle hasFeatures(AccountAuthenticatorResponse accountAuthenticatorResponse, Account account, String[] strings) throws NetworkErrorException { throw new UnsupportedOperationException(); } }
The Authenticator gets instantiated via its corresponding Authenticator Service. Implementing Authenticator Service is straightforward. Just instantiate the Authenticator object in the OnCreate method of the Authenticator Service and return its Binder object in onBind method call. Please refer to Code Snippet 6.
public class AuthenticatorService extends Service { private Authenticator mAuthenticator; public AuthenticatorService() { } @Override public void onCreate() { // Create a new authenticator object mAuthenticator = new Authenticator(this); } @Override public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } }
The Authenticator configuration xml metadata is shown below. Please note the accountType is the same as the one we used in Sync Adapter metadata. This is important as we must use the same metadata in both locations.
<?xml version="1.0" encoding="utf-8"?><account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="com.example.restaurant" android:icon="@drawable/ic_launcher" android:smallIcon="@drawable/ic_launcher" android:label="@string/app_name"/>
Finally, we need to create Android Manifest entry for Authenticator Service, please see code snippet 8. Notice the above authenticator metadata is referred by android:resource under meta-data.
<service android:name=".AuthenticatorService" android:enabled="true"><intent-filter><action android:name="android.accounts.AccountAuthenticator" /></intent-filter><meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /></service>
Configuring and Triggering the Sync
Now that we have all the pieces—Content Provider, Sync Adapter, and Authenticator in place—we just need to tie it all together to be able to trigger a sync whenever required. Well, technically Android Framework automatically does all the magic, but we still need to configure the trigger.
As discussed earlier, there are several ways to trigger a sync. For the sample app, we use an incoming GCM notification with sync attribute as the trigger. We can also trigger the sync at app start up time, or maybe in OnCreate or OnResume of the Main Activity.
private Account createDummyAccount(Context context) { Account dummyAccount = new Account("dummyaccount", "com.example.restaurant"); AccountManager accountManager = (AccountManager) context.getSystemService(ACCOUNT_SERVICE); accountManager.addAccountExplicitly(dummyAccount, null, null); ContentResolver.setSyncAutomatically(dummyAccount, RestaurantContentProvider.AUTHORITY, true); return dummyAccount; } @Override protected void onResume() { super.onResume(); checkGooglePlayServices(); ContentResolver.requestSync(createDummyAccount(this), RestaurantContentProvider.AUTHORITY, Bundle.EMPTY); }
We use a dummy account with “com.example.restaurant” accountType, which we configured in both Sync Adapter and Authenticator xml metadata. We also explicitly call setSyncAutomatically on ContentResolver, as it is required. This can be avoided if you specify android:syncable=“true" on your “provider” Android Manifest entry.
The actual Sync request is done using the ‘requestSync’ method on ContentResolver.
We can issue the same on-demand sync method call when we receive a GCM notification in Broadcast Receiver.
public class GcmBroadcastReceiver extends BroadcastReceiver { private static final String TAG = GcmBroadcastReceiver.class.getSimpleName(); public GcmBroadcastReceiver() { } @Override public void onReceive(Context context, Intent intent) { GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context); if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(gcm.getMessageType(intent)) && intent.getExtras().containsKey("com.example.restaurant.SYNC_REQ")) { Log.d(TAG, "GCM sync notification! Requesting DB sync for server dbversion " + intent.getStringExtra("dbversion")); ContentResolver.requestSync(new Account("dummyaccount", "com.example.restaurant"), RestaurantContentProvider.AUTHORITY, Bundle.EMPTY); } } }
This will trigger the background sync anytime the server sends a GCM notification with sync attribute.
About the Author
Ashok Emani is a software engineer in the Intel Software and Services Group. He currently works on the Intel® Atom™ processor scale-enabling projects.