12
1 Handling Offline Capability and Data Sync in an Android* App – Part 2 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.............................................................................................................................................................................................................. 1 Overview ........................................................................................................................................................................................................... 1 Data Sync Strategy for Restaurant Sample App – Little Chef. ........................................................................................ 2 Sync Adapter Implementation ........................................................................................................................................................ 4 Authenticator Implementation ....................................................................................................................................................... 7 Configuring and Triggering the Sync ....................................................................................................................................... 10 About the Author ...................................................................................................................................................................................... 11 Overview If you haven’t already read Part 1, please refer to the following link: https://software.intel.com/en-us/articles/handling-offline-capability-and-data-sync-in-an-android- app-part-1

Handling Offline Capability and Data Sync in an Android* App

Embed Size (px)

Citation preview

1

Handling Offline Capability and Data Sync in an Android* App – Part 2

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 .............................................................................................................................................................................................................. 1

Overview ........................................................................................................................................................................................................... 1

Data Sync Strategy for Restaurant Sample App – Little Chef. ........................................................................................ 2

Sync Adapter Implementation ........................................................................................................................................................ 4

Authenticator Implementation ....................................................................................................................................................... 7

Configuring and Triggering the Sync ....................................................................................................................................... 10

About the Author ...................................................................................................................................................................................... 11

Overview

If you haven’t already read Part 1, please refer to the following link:

https://software.intel.com/en-us/articles/handling-offline-capability-and-data-sync-in-an-android-

app-part-1

2

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.

3

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).

4

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); }

5

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(); } } }

6

Code Snippet 1, Sync Adapter Implementation ++

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(); } }

Code Snippet 2, Sync Service Class to instance Sync Adapter ++

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"/>

Code Snippet 3. syncadapter.xml ++

7

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>

Code Snippet 4. Sync Service entry in Android Manifest ++

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.

8

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(); } }

Code Snippet 5. Authenticator Stub Implementation ++

9

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(); } }

Code Snippet 6, Authenticator Service Class to instance Authenticator ++

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"/>

Code Snippet 7, authenticator.xml in res/xml folder ++

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" >

10

<intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service>

Code Snippet 8, Android Manifest entry for 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); }

Code Snippet 9, Create a dummy account and trigger an ondemand sync in Main Activity ++

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.

11

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); } } }

Code Snippet 10, GCM notification handling to trigger background sync ++

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.

++This sample source code is released under the Intel OBL Sample Source Code License (MS-LPL

Compatible)

Notices

12

No license (express or implied, by estoppel or otherwise) to any intellectual property rights is

granted by this document.

Intel disclaims all express and implied warranties, including without limitation, the implied warranties

of merchantability, fitness for a particular purpose, and non-infringement, as well as any warranty

arising from course of performance, course of dealing, or usage in trade.

This document contains information on products, services and/or processes in development. All

information provided here is subject to change without notice. Contact your Intel representative to

obtain the latest forecast, schedule, specifications and roadmaps.

The products and services described may contain defects or errors known as errata which may

cause deviations from published specifications. Current characterized errata are available on

request.

Copies of documents which have an order number and are referenced in this document may be

obtained by calling 1-800-548-4725 or by visiting www.intel.com/design/literature.htm.

Intel, the Intel logo, and Intel Atom are trademarks of Intel Corporation in the U.S. and/or other

countries.

*Other names and brands may be claimed as the property of others

© 2015 Intel Corporation.