Front-End Web & Mobile
Building a ContentProvider for Android
This is the second part in a six-part series on synchronizing data within an Android mobile app to the AWS Cloud. Check out the full series:
- An Introduction to the Sync Framework for Android
- Building a ContentProvider for Android (this article)
- Using a ContentProvider in Android Mobile Apps
- Integrating Amazon Cognito with the Android AccountManager API
- Building a Synchronization Endpoint with AWS Mobile Hub
- Building a Synchronization Framework for Android
In the previous post, we described data management in Android and the role of the content provider and sync framework in building cloud-connected data-driven applications. In this article, we delve into the content provider in some detail. Building a content provider is a multi-step process:
- Create a model for handling the data.
- Create a ContentContract that defines the content provider constants.
- Create a ContentProvider to access the data.
- Define the content provider within the application manifest.
In this post, we show how to write a content provider for a simple list of notes. The long-term goal is to synchronize that data to the cloud. You can also use tasks, news stories, comments, or any other data type. It is possible to use multiple data types within the same content provider.
Create a model
When building the class for your model, you need to think about the eventual use case. Since this model is going to be synchronized, there are additional fields to store on the client side. The server-side model needs to be multi-user, but that information does not need to be transferred to the client side. Here are the two models:
Most fields are transferred complete. There are three special cases:
- isDirty only exists on the client side and is used to indicate that the client version of the record needs to be transferred to the server.
- userId only exists on the server side. The current user owns all the records on the client, so it is not needed. When submitting records on the server, you tag each record with the userId.
- isDeleted is different on the server and client. When isDeleted is set on the client, that value is sent to the server. When isDeleted is received from the server, the record is removed from the client.
Of the other fields, noteId is a globally unique identifier (GUID), updated is a time stamp (represented in UTC by the number of milliseconds since the epoch). Title and content are used by the UI of the application.
It is common for the client and server models to differ. They are supporting single-user vs. multi-user and support different use cases at the query and mutation level. When developing the model for the application, ensure you can read and write JSON data on each side. The transfer to the server uses JSON formatted data, so handling this now is a good idea. You can use the gson library to perform the conversion. Add the following to your app/build.gradle in the dependencies section:
The former does the JSON conversion and the latter provides utility methods for building checksum or hash code functions that you need to generate models. You can auto-generate a template using the jsonschema2pojo website. This allows you to convert a JSON object into the model library. Ensure you specify Gson annotation style and use primitive types. You need to tweak the model afterwards. Here is the completed version:
All of the fields that you send to the server have a SerializedName and an Expose annotation. The isDirty field does not have these annotations because it is not exposed to the server. We also use two helper methods from the Apache Commons Lang library for handling hash codes and equality. You only use them to verify that the critical fields are equal. The isDeleted and isDirty fields are not germane to that decision so they are left off the comparison. The id field is not used by the app but is required by the SQLite implementation of the content provider, so it is necessary in the client-side model.
Create a content contract
The next step is to create a content contract. The content contract is a series of constants and methods that allow the application to effectively use the content provider. They provide the base URI of the tables and the representation of the tables in the database. First, you define how the provider is referenced. The URI is something like the following:
content://com.amazonaws.mobile.samples.notetaker.provider/notes[/noteId]
The com.amazonaws.mobile.samples.notetaker.provider is called the Authority and it is a unique value (across all applications on the device) that represents the dataset. Because the value is unique across all applications on the device and you cannot know which applications are on the device, this value is effectively unique in the universe. Although not required, it is normal to make it look like your java package name. Minimally, your content contract must contain the authority and content URI:
In addition, provide an inner class per table for each data type. In this case, we only have one model, so we only have one inner class. The inner class must specify:
- The table name
- The field names
- The URI forms that are acceptable for the table
- How to construct a URI from an ID
In this example, the table name (within SQLite) is notes and the field names are the names of the fields in the client-side model. Because this is a SQLite implementation, you must have an id field that is an auto-incrementing integer. This is not used by the application and is an artifact of the database.
Create a content provider
This content provider is based on the SQLite database. The Android system provides assistance for SQLite databases so this is the path of least resistance. You may, however, need to switch to a NoSQL offline cache, such as Couchbase Lite, if your data model does not fit into a relational database model. This takes more work because you need to deal with the queries yourself. First, create a database helper:
When DatabaseHelper is created, it creates a database if needed, calling onCreate() to set up the database structure. It also stores a database version. If the database version changes (for example, you add a new field), increase DBVERSION in DatabaseHelper. The database helper then calls onUpgrade() and you can use db.execSQL() to adjust the database by using SQL statements. With this class, the effort of maintaining the database connection and maintaining the schema is done for you.
The content provider itself must implement six methods:
- onCreate() is called when the content provider is first accessed.
- getType() returns the MIME type of a provided URI.
- query(), insert(), delete(), and update() are called for CRUD operations on the provider.
The standard form for the content provider is as follows:
We didn’t fill in the actual methods yet, except for onCreate(), which initializes the database helper and creates the database if required. Note the URI matcher. When you call the URI matcher with a URI, it converts the matched URI to an integer. You can then use this in a switch statement, which makes most of the code easier to understand. For example, here is the getType() method:
Without the URI matcher, we would have to deconstruct the URI to ensure that the URI was handled by this content provider. We also would be required to use a regular expression parse on each type that we handle. The URI matcher makes this much easier.
The three mutation methods write to the database using standard SQLite routines. Think of these as REST endpoints. To insert a new item, you POST to the directory URI. To update an entry, you POST (to replace) or DELETE to the item URI.
When you update or delete one item, you prepend a search for the task ID to the SQL query. Other applications (including widgets and the sync framework) can listen to the content resolver for changes. If the database changes, notify any listeners that the data has changed. Listeners can then act on that change, by updating the widget or immediately pushing the change to the server, for example. The content provider enables you to update or delete multiple records. For example, you might want to delete all records that are marked for deletion, or update all incomplete tasks to be completed. However, inserts are handled individually.
That leaves the query portion. The query takes a SQL query and returns a Cursor that can be used to load the data into the UI. The whole process is driven by the UI. This is well suited for a UI-driven application like a mobile app.
Again, the SQLite database is helpful here. You can move directly from the arguments that are fed to the content provider query() method to the database query itself. Everything happens naturally from this. A different database provider would require building your own query, which is likely to be more complex code within the content provider. However, the changes to support a different local database are confined to the content provider. This application does not need to know about the changes to the underlying database.
Register the content provider
Before the app can use the content provider, it must be registered in the AndroidManifest.xml file. This is defined by a <provider> element that must be located with the <application> element for the Android application:
The android:name field for the provider points to the class. If the class starts with a period, then it is relative to the package referenced in the manifest. The android:authorities field must match the AUTHORITY field in the content contract. The label is required but can be anything you like because it is not displayed anywhere.
Wrap up
We’ve only just started to investigate the content provider, and the code doesn’t run yet! In the next post, we use this content provider to produce a simple task list app and show how to call this content provider (or any other content provider) from your code. If you can’t wait until the next article, the content provider was taken from our Notes tutorial code that is published on GitHub.