WHAT’S IN THIS CHAPTER?
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
Please note that all the code examples in this chapter are available at https://github.com/wileyenterpriseandroid/Examples.git and as a part of the book’s code download at www.wrox.com on the Download Code tab.
As you’ve read in this book, mobile developers face similar problems on many platforms when writing applications that use network services to store data. Developers getting started with Android face problems associated with network data loading right off the bat. A typical first application for many developers will include a list of items loaded from a network service. The items will use thumbnails that require lazy loading. Developers with a bit more experience will start to use more sophisticated techniques like paging schemes that avoid the need to load all the results of a query over the network — large queries are loaded in pieces. Developers will also need a RESTful protocol for communicating with a backend service, and that protocol is likely to need synchronization support to deal with multiple hosts changing the same state. Developers often want offline data editing that allows an app to remain useful if the network becomes unavailable. Ideally, developers won’t have to reinvent the wheel to address concerns such as these, but Android out of the box could do more to help developers solve these problems correctly.
As noted in earlier chapters, when implementing these functions, developers are likely to create similar bugs such as long-running UI events, hanging UI threads, or inflexible code that requires frequent application updates. Developers often write inefficient network protocols that rely on polling to update application data.
Why not create a system that solves all of these problems in a generic way that can be reused for many applications?
Of all the expressive web services and built-in content providers available on mobile platforms today, we see a gap in the available features — a generic data service explicitly designed to plug into a mobile MVC client in a way that extracts the redundantly implemented chores of handling data from the user interface of a mobile application.
This chapter introduces a new and simple RESTful network API that leverages the Android content provider system to facilitate seamless custom data integration into the Android platform. It covers a service implementation of that API and a mobile client for the API that communicates directly with user interface components. The chapter ends by summarizing advantages of deploying applications around an architecture that uses this technology. The next chapter digs into writing some real applications using this system.
The WebData API and an open source implementation of it called, project Migrate provide a solution in this space. These technologies relieve Android developers from the need to write their own SQL tables, create their own synchronized and RESTful protocol, and integrate with the Android view components in a way that is resilient to all of the problems described in this and previous chapters. At the time of writing of this book, Migrate and WebData represent a fledgling open source effort. Consequently, you should expect to see significant changes in the project in the coming months. The Migrate feature set currently just puts “meat on the bones” of a compelling set of ideas and in terms of features that can make this system convenient for developers, the project has room to grow. For this reason, this chapter delves into both the existing Migrate implementation and important future enhancements for the project.
We’ve focused on the third pattern from Chapter 5, content providers used with sync adapters, and created a specific RESTful protocol that supports robust Android network communication for custom data with synchronization support. The Migrate project provides a generic data binding framework that links standard Android UIs to backend service persistence on a remote host. The API is geared toward a persistent key/value interface like that of android.content.Cursor that enables WebData service implementations to easily leverage web-scale databases like DataStore, MongoDb, DynamoDb, etc.
With WebData, developers use a schema format to define custom data in a backend service. The Migrate client, an Android-based content provider and sync adapter, can synchronize this data to a device and bind it into UI components. An Android application UI only needs to communicate with an Android-based API to store and access data (for example, a provider contract). Project Migrate handles the REST (pun intended). The Migrate provider API shares characteristics of built-in Android content providers, like the calendar and contacts providers, but also enables applications to define their own types of data and reuse a generic framework to create data that can serve a diverse range of applications.
The WebData API enables developers to define and synchronize persistent “objects” or map oriented key/value pairs (like JavaScript objects). The Migrate system takes care of persisting and synchronizing changes in these objects between a backend service with scalable storage, and mobile clients using SQLite over time. The API is event oriented, so it fits seamlessly into the network-oriented MVC pattern discussed in Chapter 5.
Figure 9-1 provides an illustration of the Migrate and WebData architecture with an Android client.
The following characteristics of the WebData API and project Migrate drastically reduce the amount of overhead required to create robust Android network applications:
The WebData API reuses standard web services concepts wherever possible in order to permit as wide a range for integration as possible (we currently support an Android client, foresee an iPhone client, and may eventually provide HTML5 integration). The API stipulates a serialization format based on JSON, which is both widely deployed and efficient, and on JSON schema, a simple object schema format. The API also requires RESTful URL-based protocol requests. Here’s a complete list of the components of the WebData API specification:
The next sections provide detailed descriptions of relevant aspects of the WebData API protocol. As you read, keep in mind that the Migrate WebData client API relies on the standard content provider API CRUD operations and sync adapter synchronization to access and update WebData objects.
The following explanations of protocol operations only serve as an overview for developers who want to understand the details of the Migrate client and synchronization protocol. In explicit terms, the Migrate client directly uses these operations; UI developers do not. Note also that we have structured the design of protocol operations to preserve the standard sequence of the built-in providers — the CRUD operations operate on local SQLite state, and the synchronization operation is the only time that the provider communicates with the backend service.
The WebData API specifies a REST-based synchronization protocol. The API revolves around schema that define data, REST operations that modify persistent objects, and a synchronization system that uses those operations to keep data consistent between a service and multiple clients.
Before a client can use a WebData service to store domain objects, the service must have schema information that defines those objects. WebData uses JSON schema, http://json-schema.org/, as an object schema format, and a WebData JSON envelope to support synchronization. JSON schema is an extremely simple object definition language that supports the declaration of an array of domain objects with corresponding <key, type> properties that define fields of those objects.
Applications that need to define new schema, simply POST JSON schema to WebData service instances. The POSTing entity will typically be a backend service itself, rather than a mobile client. Using a third party program to define schema allows the schema to change in backward-compatible ways (for example, fields used by clients are not removed, and so forth) after an application is deployed.
The schema in Listing 9-1 defines a domain object for a contact usable in a hypothetical contacts application. To define this contact type, you’ll need a utility that POSTs the schema and Sync envelope into the following WebData service URL:
http://host:port/contacts/scheme/com.enterpriseandroid.webDataContacts.dataModel.Contact
Figure 9-2 shows a web application posting a schema into a WebData service implementation. After the web app POSTs this schema, clients can read and write objects with fields that conform to the schema.
The schema payload includes the synchronization envelope and the schema itself. Listing 9-1 shows the contact POST.
LISTING 9-1: A WebData schema creation POST payload
1 POST
http://host:port/context/schema/com.enterpriseandroid.webDataContacts.Contact
2 {
3 "wd_version":1,
4 "wd_id":"com.enterpriseandroid.webDataContacts.Contact",
5 "wd_classname":"com.migrate.webData.model.PersistentSchema",
6 "wd_namespace":"__schema",
7 "wd_deleted":false,
8 "wd_status":0
9 "jsonSchema":{
10 "properties":{
11 "status":{
12 "type":"string"
13 },
14 "lastname":{
15 "type":"string"
16 },
17 "email":{
18 "type":"string"
19 },
20 "age":{
21 "type":"integer"
22 },
23 "birthDate":{
24 "type":"integer"
25 },
26 "phoneNumber":{
27 "type":"string"
28 },
29 "wd_id":{
30 "required":true,
31 "type":"string"
32 },
33 "firstname":{
34 "type":"string"
35 },
36 "wd_namespace":{
37 "type":"string"
38 },
39 "wd_classname":{
40 "type":"string"
41 },
42 "wd_updateTime":{
43 "required":true,
44 "type":"long"
45 },
46 "wd_version":{
47 "required":true,
48 "type":"integer"
49 },
50 "wd_deleted":{
51 "required":true,
52 "type":"integer"
53 },
54 },
55 "type":"object"
56 },
57 }
We want to take a minute to point out some interesting details about this code:
WebData supports both CRUD and Sync based access to schema and domain data. For example, the following URI format provides access to contact objects with fields defined by the schema in the previous section:
http://host:port/contacts/classes/com.enterpriseandroid.webDataContacts.api.Contact/{id}
The general form of WebData data URIs follows:
http://host:port/context/classes/{class}/{id}
where class denotes the schema id of the requested object, and id indicates the UUID of the object. GET, PUT, POST, and DELETE operations on URIs like the one shown here have the standard RESTful effect — POST creates an object, PUT updates it, GET returns it, and DELETE removes the object. Requests select specific object instances using the UUID. The next few code listings show significant example protocol requests, to give you a sense of how WebData works.
Listing 9-2 shows an example POST payload that creates a new contact instance.
LISTING 9-2: An example domain object POST request
1 POST
http://host:port/context/classes/com.enterpriseandroid.webdataContacts.Contact/
b83296c4-2bdb-438e-a789-57536431026c
2
3 {
4 "wd_version":1,
5 "wd_namespace":"__data",
6 "wd_deleted":0
7 "firstname":"John",
8 "lastname":"Smith",
9 "birthDate":136194847583,
10 "email":"[email protected]",
11 "age":23,
12 "phoneNumber":"978-123-4567",
13 "status":"some status",
14 }
This POST creates a new WebData object with data and envelope fields described as follows:
Listing 9-3 shows a WebData GET request for the object posted in Listing 9-2. Such a request would return a JSON-formatted WebData object, as listed.
LISTING 9-3: An example WebData GET response
1 GET
http://host:port/context/classes/com.enterpriseandroid.webDataContacts.Contact/
b83296c4-2bdb-438e-a789-57536431026c
2 {
3 "wd_id":"b83296c4-2bdb-438e-a789-57536431026c",
4 "wd_version":1,
5 "wd_classname":"com.enterpriseandroid.webDataContacts.Contact",
6 "wd_namespace":"__data",
7 "wd_updateTime":1369022907831,
8 "wd_deleted":0
9 "firstname":"John",
10 "lastname":"Smith",
11 "birthDate":136194847583,
12 "email":"[email protected]",
13 "age":23,
14 "phoneNumber":"978-123-4567",
15 "status":"some status",
16 }
The response retrieves the formerly POSTed contact object. The following highlights the lines that should catch your interest:
The WebData API supports CRUD-based queries; however, the typical usage pattern for Migrate at the time of writing of this book is to rely on synchronization and local content provider operations. A future edition of this book may cover more detail on Migrate CRUD operations, such as remote search. For now, Migrate relies on the ContentProvider.query method to enable searching for data in content provider managed SQLite tables.
WebData specifies support for push messaging notification to prevent clients from needing to poll a WebData implementation service for data changes. The WebData payload for push messages merely contains a list of schema identifiers that indicate the data that has changed since the last client update. Once the client has received this list from the push notification, it should engage in the synchronization protocol outlined in the next section for each modified schema identifier. Listing 9-4 shows an example WebData schema modification notification, where lines 4 and 5 list the schema ids for which data has changed.
LISTING 9-4: An example WebData schema modification notification
1 {
2 "modified":
3 [
4 "com.enterpriseandroid.webDataContacts.Contact",
5 "com.enterpriseandroid.webDataAutomobile.Automobile"
6 ]
7 }
Data travels or “migrates” between a WebData client and a WebData service instance during a synchronization operation. Such operations are central to the concept of the WebData API. These exchanges of data allow clients to merge local changes with remote changes in a service host, and also represent the mode of transport of the objects between the client and the service. The WebData API specifies synchronization support according to the following protocol:
Because the WebData API permits concurrent changes on multiple devices, it’s possible for conflicts to arise between data modifications from different sources. The WebData API specifies the transmission of version information to enable the resolution of versioning conflicts when data is modified between the backend service and multiple clients. WebData objects must always contain a version field that is incremented every time the client or service succeeds in changing a given object. When a client updates an object, the server checks the version against the one stored on the server. The update operation succeeds only when the version numbers match.
WebData implementations should resolve conflicts as follows:
These operations should look similar to the ones outlined in Chapter 6, which intentionally built a foundation for WebData synchronization as discussed earlier in this chapter.
To help you understand how synchronization and conflict resolution works in practice, we’ve created a simple scenario in which:
The client will need to sync the server contact that is not in conflict (that is, make its own copy locally) and will need to resolve differences between its own version of the conflicted contact and the server’s version. Listings 9-5, 9-6, and 9-7 show these changes and Figure 9-3 illustrates the interaction. Changes are flowing in and out of the local SQLite cache on the client. Changes from the client and service are selected based on timestamp, similar to what you saw in Chapter 6.
The scenario begins with Listing 9-5, when a client initiates a sync request.
LISTING 9-5: A client initiates sync with objects changed since last sync time
1 POST http://host:port/migrate/context/classes/
com.enterpriseAndroid.webDataContacts.dataModel.Contact?syncTime=13423220558251
2
3 {
4 "modified":[
5 {
6 "firstname":"Mark",
7 "lastname":"Johnson",
8 "birthDate":135579679583,
9 "email":"[email protected]",
10 "age":43,
11 "phoneNumber":"781-201-4567",
12 "status":"some status",
13 "wd_id":"a7329678-4bdc-536a-b234-78236431026a",
14 "wd_version":1,
15 "wd_classname":"com.enterpriseandroid.webDataContacts.Contact",
16 "wd_namespace":"__data",
17 "wd_updateTime":1369010208846,
18 "wd_deleted":0
19 }
20 ],
21 "resolved" : []
22 }
The sync request shows the client has changed a contact “Mark Johnson”. The following points explain the payload:
The service responds to the sync request with the payload in Listing 9-6.
LISTING 9-6: The service response — the service has changed two contacts and one modification has resulted in a conflict
1 {
2 "syncTime":13690102023423,
3 "modified":[
4 {
5 "firstname":"Andrew",
6 "lastname":"Smith",
7 "birthDate":135579679583,
8 "email":"[email protected]",
9 "age":12,
10 "phoneNumber":"781-201-4567",
11 "status":"some status",
12 "wd_id":"d7894563-9edf-378w-u907-94675342378d",
13 "wd_version":1,
14 "wd_classname":"com.enterpriseandroid.webDataContacts.Contact",
15 "wd_namespace":"__data",
16 "wd_updateTime":1369010208846,
17 "wd_deleted":0
18 }
19 ],
20 "conflict":[
21 {
22 "firstname":"Mark",
23 "lastname":"Johnson",
24 "birthDate":135579679583,
25 "email":"[email protected]",
26 "age":43,
27 "phoneNumber":"781-223-1234",
28 "status":"some status",
29 "wd_id":"a7329678-4bdc-536a-b234-78236431026a",
30 "wd_version":2,
31 "wd_classname":"com.enterpriseandroid.webDataContacts.Contact",
32 "wd_namespace":"__data",
33 "wd_updateTime":1369010208846,
34 "wd_deleted":0
35 }
36 ]
37 }
The server response contains a new contact for “Andrew Smith” that the client can simply add locally. However, the contact with an id "a7329678-4bdc-536a-b234-78236431026a" for “Mark Johnson” is in conflict. The version of this contact from Listing 9-5 has a phone number of 781-201-4567, and the server has a version of the contact with phone number 781-223-1234. The Migrate client will call back to the relevant Android UI to ask the application user to pick the right phone number. Points to note in the payload include:
To resolve the conflict in the service, the client needs to POST the resolved object back with the new version in another sync request as follows (Listing 9-7).
LISTING 9-7: The client resolves the conflicting phone number
1 POST
http://host:port/migrate/contact/classes/
com.enterpriseAndroid.webDataContacts.Contact?syncTime=1342322066851
2 {
3 "modified":[],
4 "resolved":[
5 {
6 "firstname":"Mark",
7 "lastname":"Johnson",
8 "birthDate":135579679583,
9 "email":"[email protected]",
10 "age":43,
11 "phoneNumber":"781-223-1234",
12 "status":"some status",
13 "wd_id":"a7329678-4bdc-536a-b234-78236431026a",
14 "wd_version":2,
15 "wd_classname":"com.enterpriseandroid.webDataContacts.Contact",
16 "wd_namespace":"__data",
17 "wd_updateTime":1369010208846,
18 "wd_deleted":0
19 }
20 ]
21}
With the sync request in Listing 9-7, the client has selected the desired phone number on line 11, and used the same version as was sent to it as a conflict from the service, on line 14. The service will see that this is a resolution request, from line 4, and apply the change from the client to increment the object to version 3, assuming no other conflicting changes happen in the interim — which demonstrates optimistic concurrency control as applied to WebData.
Now that you understand WebData synchronization, it’s time to take a look at some other features.
Polling for changes in data goes somewhat against the grain of the design of the WebData API. In the absence of a push notification system, a WebData client should periodically invoke synchronization to ensure data stays current for a particular schema identifier. In Android, polling should be invoked using the Android sync adapter API ContentResolver.requestSync.
The WebData API provides parameters for paging that enable a client to download subsets of results when a query would return a large number of objects. The client can use these paging controls to conserve memory and storage space as needed. The following URL query parameters provide paging controls to a WebData client:
The following GET request shows an example query with paging parameters:
GET http://host:port/context/classes/{classname}?startPosition=30&maxSize=10
Now that you’ve learned a bit about the components of the WebData API, it’s a good time to look at the complete WebData specification, which is available at the following location:
https://github.com/wileyenterpriseandroid/migrate/wiki/WebData
The project Migrate client currently provides a complete implementation of the WebData API and supports an API for UI integration on the Android platform. Future versions of Migrate may provide iPhone and Objective C support, and potentially support for JavaScript. However, this book focuses on Migrate support for Android. As of the time of writing of this book, the Migrate open source project supports a working Android client implementation.
The Migrate project supports an Android client that leverages the Android content provider framework. Although the WebData API may be useful on IOS and on other platforms, it’s not an exaggeration to say that the API was designed to take advantage of the Android content provider infrastructure. Recall that with project Migrate, developers can create their own synchronized data services that offer the convenience of the built-in Android content providers, but allow application-defined data types. The project Migrate Android client uses the service schema definition to create local SQLite tables as needed to store synchronization data. The client also uses the WebData versioning protocol to update data and schema information as directed by the WebData service host.
The bulk of the Migrate WebData client on Android resides inside a custom Android content provider. This provider supports the following features that help developers manage data for Android mobile applications:
As mentioned, there are a couple of ways in which the Migrate content provider differs from the built-in providers, specifically:
The project Migrate Android client uses the WebData synchronization protocol to maintain a local persistent SQLite-based data cache that tracks changes from the client and service, updating and replacing elements that change in the service host. The Migrate WebData Android client implements the WebData synchronization protocol using a sync adapter implementation, using the standard onPerformSync method, as shown in the pseudo-code in Listing 9-8:
LISTING 9-8: Illustrating sync from onPerformSync
1 onPerformSync(Account account, Bundle extras, String authority,
2 ContentProviderClient provider, SyncResult syncResult)
3 {
4 // The Migrate client synchronizes with its service host
5 Map serverData =
6 WebDataClient.syncData(className, values, lastUpdateTime);
7 . . .
8 }
A client that needs to query Migrate data should just use the standard ContentProvider.query method.
The Migrate client implementation can use Google Cloud messaging for Android to avoid polling the Migrate service. In the absence of push notification support, the Migrate client can use a polling system that operates out of band of the normal WebData synchronization protocol. The implementation can periodically invoke a WebData synchronization operation.
Google has made documentation available on its website:
http://developer.android.com/google/gcm/index.html
Now that you have seen the features offered in the Migrate client and its content provider, this section explains how to access Migrate data in an Android UI. Currently, the Migrate project supports a slightly modified version of the API style used by the built-in Android content providers.
The APIs of built-in Android content providers each revolve around a contract class, like the one for the contacts provider, ContactsContract:
http://developer.android.com/reference/android/provider/ContactsContract.html
The central purpose of the Android built-in content provider APIs is to host constant fields in classes like ContactsContract.Email.DATA, which can be used to index the Cursor results of invoking a query method as follows:
Cursor email = ContentResolver.query();
final Cursor email = ... // use of LoaderManager to access an email query cursor
final int contactEmailColumnIndex = email.getColumnIndex(Email.DATA);
String emailData = email.getString(contactEmailColumnIndex);
The last two lines show the indexing of a cursor to obtain its e-mail data.
These fields — in combination with content provider URIs as discussed in Chapter 4 — compose the APIs that developers use to access the Android contacts data and that of other built-in providers. These column classes (ContactsContract.Email, ContactsContract.Settings, ContactsContract.StatusUpdates, and so on) map to SQLite database tables that provide content provider persistence. This API approach relies on a set of hard-coded constants to access the central Android key/value-oriented data structure, Cursor. These constants must be “well known” to the application developer in order to load cursor data.
The Migrate provider API uses the same style of contract API as discussed in the previous section; however, the usage model has a significant difference: Migrate provides a utility in its SDK that generates the code for a contract class given a service POJO with Migrate annotations as input. The same tool also creates a WebData schema for POSTing to a WebData service instance. The next chapter demonstrates how this works in a detailed example.
This chapter has covered the WebData protocol and its application in the Migrate Android open source project. Migrate has significant potential for Enterprise Android application development. Much of this book has built a foundation in technical understanding that enables you to appreciate the benefits of using Migrate to bridge the gap between enterprise Android applications and scalable cloud infrastructure.
Now that you have learned about the operation of the WebData API, the Migrate backend service, and the Migrate Android client, the chapter concludes with a discussion of the benefits of deploying a mobile infrastructure based on a WebData style architecture.
The WebData API encourages efficient network communication that reduces service load and enhances application protocols in the following ways:
Properties of WebData and the Migrate client that benefit handset applications include the following:
Now that you’ve looked at the WebData and Migrate Android APIs in detail, you’ll jump into building an actual application with project Migrate in the next chapter.