Wednesday, September 25, 2013

Android Remote Sync Content Provider Pattern

If you plan on creating an Android SyncAdapter, you should have already seen the great Google I/O presentation on the subject already

The presentation describes how to design your database to keep track of the sync state for each database row you need to sync. Essentially, the database becomes the mediator between an Activity and the SyncAdapter.

Sync States
Here I've listed the SyncState enum I use to keep track of record state. For example, 'created' is used by the Activity and 'creating' is using by the SyncAdapter.

While the pattern is powerful for all of the reasons described in the video, it does put a burden on the Activity developer to manage the state whenever a record is changed. It also becomes fairly messy in the ContentProvider to determine whether the change was made by the Activity and is outgoing, or was made by the SyncAdapter and is, thus, incoming. So I sought a better way to encapsulate the client-side responsibility of this pattern in some kind of proxy.

  1. Keep the client DRY; all of the state management code is redundant
  2. Align with the Android ContentProvider interface; all of the CRUD methods available on the ContentProvider already describe exactly what the client needs to manage the sync state
Divide and Conquer
Since the Activity only interacts with the database through a ContentProvider, I created my proxy solution using another provider I called RemoteSyncContentProvider. My original ContentProvider is now called LocalContentProvider because it interacts with the database only, and is only used by the RemoteSyncContentProvider and the SyncAdapter. Since I ended up with some fairly common aspects of the two ContentProviders, the common parts are shared in a common base called AbstractContentProvider.

Since I have two ContentProviders, I have two content authorities: Contract.authority and Contract.authorityLocal. The local authority is not tied to any SyncAdapters as it represents the local database.

The first benefit of splitting the responsibilities is that the LocalContentProvider now becomes much simpler; it updates the local database and never needs to syncToNetwork -- it is also agnostic of the fact that the sync states exist at all.

The gist of the RemoteSyncContentProvider is that it implements query, insert, update, delete utilizing an instance of the LocalContentProvider client that it got though getContext ().getContentResolver ().acquireContentProviderClient (Contract.authorityLocal). Since the entire purpose of the RemoteSyncContentProvider is to manage the sync state of each row that it touches, before proxying to the LocalContentProvider, it sets the necessary SyncState value on the ContentValues passed to it.

For insert, it always sets the 'create' sync state. For update, it sets the 'update' sync state unless the sync state is already 'create'. The RemoteSyncContentProvider can also decide if of these changes should force a resync of some related tables that should now be fetched because of the new values.

Delete is a little more work since it first has to query the sync state. If the sync state is 'create', it directly delegates to the delete call, otherwise it updates the sync state to 'delete'.

To support queries of records where only partial information about them is known (some kind of key identifying columns), the RemoteSyncContentProvider implements query by inserting stub records with the key columns and the read state. To make sure the query results are available in a reasonable amount of time without having to wait for the next sync tickle, the RemoteSyncContentProvider will force the SyncAdapter to resync using ContentResolver.requestSync() with the manual and expedited flags.

In all of the content modifier methods, notifyChange is called with syncToNetwork true since this is the RemoteSyncContentProvider and all of these changes need to initiate the SyncAdapter to process these database changes. Note that the authority tied to the RemoteSyncContentProvider must match the authority of the SyncAdapter so that syncToNetwork triggers the right SyncAdapter.

In the SyncAdapter itself, however, while the RemoteSyncContentProvider is the ContentProvider passed to it as the ContentProviderClient, so this client must be ignored to prevent sync update loops. Instead, a separate instance of the LocalContentProvider (authorityLocal) is used by the SyncAdapter (make sure to release() it).

As described in the Google I/O presentation, the SyncAdapter is very aware of the sync states it finds through the local authority. It uses these states to determine which records need to be pushed to and pulled from the remote server; it updates these states while it is processing the sync so the sync status is always available in the database and communicated to CursorAdapters automatically. Finally, when the sync is complete and successful, the SyncAdapter clears the sync states to indicate completion and to prevent a resync of the same records.

In the end, a client Activity can use the RemoteSyncContentProvider as any normal content provider with ignorance of the SyncAdapter doing the background work except for understanding that queries may return stub records that are asynchronously updated when there is network connectivity.

No comments:

Post a Comment