Saving our context data into a ContentProvider

Implementing a ContentProvider requires some care, as it is very easy to make a mistake. To start, we will create a new class called Provider.java in your project (e.g., com.aware.plugin.template). First thing you will need to do is to extend ContentProvider class from Android, like this:

public class Provider extends ContentProvider {
...
}

You will notice that Android Studio will notify you that you need to implement some ContentProvider methods. Press Alt + Enter after the ContentProvider and before the { and select “Implemented methods.” You will see several methods on your class, waiting to be implemented. We have simplified partly the process of creating a ContentProvider, which we recommend you to follow. You can see how we implemented any of the core ContentProviders in our repository, for example the Screen_Provider). However, we will guide you through the process.

Because a ContentProvider in AWARE is inherently shared between plugins and other applications, all the variables and methods are public.

First thing you need to define is the ContentProvider AUTHORITY. The authority is a string that is used to uniquely identify your ContentProvider within Android and it needs to be different from any other ContentProvider that might exist on your device and consequently AWARE’s repository. For the authorities in our core sensors within AWARE, we have all of them as a variant of “com.aware.provider.XXX” where XXX is the sensor name. For plugins, we will do the following: “com.aware.provider.plugin.XXX” where XXX is your plugins name.

For this example, our authority will be: com.aware.provider.plugin.example, assigned to a constant variable AUTHORITY, like this:

/**
* Authority of this content provider
*/
public static String AUTHORITY = "com.aware.provider.plugin.example";

Another important constant is the database version number (DATABASE_VERSION). The database version needs to start with number 1 (set it higher every time you change the database structure).

Every time you modify your plugin’s ContentProvider structure (i.e., add a column, change column type), you need to increase the version number (this is how Android knows that the database structure has changed and modifies your ContentProvider database file).

/**
* ContentProvider database version. Increment every time you modify the database structure
*/
public static final int DATABASE_VERSION = 1;

Now we need to decide what data will this database hold. In SQLite, there are very few database column types: integer (for indexes), text (for strings and long texts), real (precise value), blob (multimedia data – images, sound). When creating a database table, 3 columns are mandatory for the ease of database management, both to Android and AWARE. These are the mandatory columns:

  • _id: an integer column that is a primary key, automatically incremented when we insert new data;
  • TIMESTAMP: a real column that captures the phone’s current time in milliseconds (System.currentTimeMillis());
  • DEVICE_ID: a text column that captures this device’s AWARE Device ID, to uniquely identify who produced this entry.

With this in mind, we will first create a database table representation (extension of BaseColumns of ContentProvider) for use in our ContentProvider. For consistency, we use XXX_Data, where XXX is your plugin name):

public static final class Example_Data implements BaseColumns {
private Example_Data(){};
/**
* Your ContentProvider table content URI.<br/>
* The last segment needs to match your database table name
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/plugin_example");

/**
* How your data collection is identified internally in Android (vnd.android.cursor.dir). <br/>
* It needs to be /vnd.aware.plugin.XXX where XXX is your plugin name (no spaces!).
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.aware.plugin.example";

/**
* How each row is identified individually internally in Android (vnd.android.cursor.item). <br/>
* It needs to be /vnd.aware.plugin.XXX where XXX is your plugin name (no spaces!).
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.aware.plugin.example";

public static final String _ID = "_id";
public static final String TIMESTAMP = "timestamp";
public static final String DEVICE_ID = "device_id";
//... Add here more columns you might need
}

Other important variables are the URI indexes. This is what Android database management uses to find database rows or collections on your database. You will need to assign an index to each, starting with the collection index as 1 and then the item index as 2. In this table, it could look like this:

//ContentProvider query indexes
private static final int EXAMPLE = 1;
private static final int EXAMPLE_ID = 2;

The next variable required is the DATABASE_NAME, which is the name of your database as is stored on the mobile device. For consistency, plugin databases are as plugin_XXX where XXX is your plugin name, for example:

/**
* Database stored in storage as: plugin_example.db
*/
public static final String DATABASE_NAME = "plugin_example.db";

Another variable required is DATABASE_TABLES, a string array with all the tables that exist in your ContentProvider. In this example, we will only have one table. Note that the table name needs to match your CONTENT_URI last segment. In this example, the CONTENT_URI was defined as:

public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/plugin_example");

So the last segment is “/plugin_example”. This means that your table needs to match “plugin_example”. With this in mind, we declare our DATABASE_TABLES variable as follows:

/**
* Database tables:<br/>
* - plugin_example
*/
public static final String[] DATABASE_TABLES = {"plugin_example"};

We will now prepare the database SQL that is required to create the database (variable TABLES_FIELDS), based on the table database representation Example_Data. Notice that we assign a paired unique key in the end. This guarantees that we don’t have duplicate data, both locally on the phone as well as on the AWARE Server. It looks like this for our example database:

/**
* Database table fields
*/
public static final String[] TABLES_FIELDS = {
    Example_Data._ID + " integer primary key autoincrement," +
    Example_Data.TIMESTAMP + " real default 0," +
    Example_Data.DEVICE_ID + " text default ''," +
    "UNIQUE (" + Example_Data.TIMESTAMP + "," + Example_Data.DEVICE_ID + ")"
};

We will need to instantiate a UriMatcher (used to find URI paths in your ContentProvider), an HashMap<String, String> (contains your database table columns), a DatabaseHelper (a utility class we provide to manage your database) and an internal reference to your database SQLite file (SQLiteDatabase). You will also use our database initialisation function initializeDB(), like this:

private static UriMatcher sUriMatcher = null;
private static HashMap<String, String> tableMap = null;
private static DatabaseHelper databaseHelper = null;
private static SQLiteDatabase database = null;

/**
* Initialise the ContentProvider
*/
private boolean initializeDB() {
    if (databaseHelper == null) {
        databaseHelper = new DatabaseHelper( getContext(), DATABASE_NAME, null, DATABASE_VERSION, DATABASE_TABLES, TABLES_FIELDS );
    }
    if( databaseHelper != null && ( database == null || ! database.isOpen()) ) {
        database = databaseHelper.getWritableDatabase();
    }
    return( database != null && databaseHelper != null);
}
/**
 * Allow resetting the ContentProvider when updating/reinstalling AWARE
 */
 public static void resetDB( Context c ) {
     Log.d("AWARE", "Resetting " + DATABASE_NAME + "...");

     File db = new File(DATABASE_NAME);
     db.delete();
     databaseHelper = new DatabaseHelper( c, DATABASE_NAME, null, DATABASE_VERSION, DATABASE_TABLES, TABLES_FIELDS);
     if( databaseHelper != null ) {
         database = databaseHelper.getWritableDatabase();
     }
 }

We now need to implement the unimplemented ContentProvider functions that are used to manage our database, according to how we defined our table and columns.

delete()

What happens when you delete data from your database

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    if( ! initializeDB() ) {
        Log.w(AUTHORITY,"Database unavailable...");
        return 0;
    }

    int count = 0;
    switch (sUriMatcher.match(uri)) {
        case EXAMPLE:
            count = database.delete(DATABASE_TABLES[0], selection,selectionArgs);
        break;
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
    getContext().getContentResolver().notifyChange(uri, null);
    return count;
}

getType()

What your database returns as content URI when you access your database

@Override
public String getType(Uri uri) {
    switch (sUriMatcher.match(uri)) {
        case EXAMPLE:
            return Example_Data.CONTENT_TYPE;
        case EXAMPLE_ID:
            return Example_Data.CONTENT_ITEM_TYPE;
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
}

insert()

What happens when you insert new data to your database

@Override
public Uri insert(Uri uri, ContentValues new_values) {
    if( ! initializeDB() ) {
        Log.w(AUTHORITY,"Database unavailable...");
        return null;
    }

    ContentValues values = (new_values != null) ? new ContentValues(new_values) : new ContentValues();

    switch (sUriMatcher.match(uri)) {
        case EXAMPLE:
            long _id = database.insert(DATABASE_TABLES[0],Example_Data.DEVICE_ID, values);
            if (_id > 0) {
                Uri dataUri = ContentUris.withAppendedId(Example_Data.CONTENT_URI, _id);
                getContext().getContentResolver().notifyChange(dataUri, null);
                return dataUri;
            }
            throw new SQLException("Failed to insert row into " + uri);
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
}

onCreate()

What happens when Android initialises your database

@Override
public boolean onCreate() {
    AUTHORITY = getContext().getPackageName() + ".provider.plugin.example"; //make AUTHORITY dynamic
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(AUTHORITY, DATABASE_TABLES[0], EXAMPLE); //URI for all records
    sUriMatcher.addURI(AUTHORITY, DATABASE_TABLES[0]+"/#", EXAMPLE_ID); //URI for a single record

    tableMap = new HashMap<String, String>();
    tableMap.put(Example_Data._ID, Example_Data._ID);
    tableMap.put(Example_Data.TIMESTAMP, Example_Data.TIMESTAMP);
    tableMap.put(Example_Data.DEVICE_ID, Example_Data.DEVICE_ID);

    return true; //let Android know that the database is ready to be used.
}

 query()

Allow the user to query your database table

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
    if( ! initializeDB() ) {
        Log.w(AUTHORITY,"Database unavailable...");
        return null;
    }

    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    switch (sUriMatcher.match(uri)) {
        case EXAMPLE:
            qb.setTables(DATABASE_TABLES[0]);
            qb.setProjectionMap(tableMap);
        break;
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
    try {
       Cursor c = qb.query(database, projection, selection, selectionArgs, null, null, sortOrder);
       c.setNotificationUri(getContext().getContentResolver(), uri);
       return c;
    } catch (IllegalStateException e) {
       if (Aware.DEBUG) Log.e(Aware.TAG, e.getMessage());
       return null;
    }
}

update()

Allow the user to update existing records on the database

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    if( ! initializeDB() ) {
        Log.w(AUTHORITY,"Database unavailable...");
        return 0;
    }

    int count = 0;
    switch (sUriMatcher.match(uri)) {
        case EXAMPLE:
            count = database.update(DATABASE_TABLES[0], values, selection, selectionArgs);
        break;
        default:
            database.close();
            throw new IllegalArgumentException("Unknown URI " + uri);
    }
    getContext().getContentResolver().notifyChange(uri, null);
    return count;
}

We now need to add this ContentProvider to our plugin’s manifest, while demanding the AWARE permissions to access its data. On the AndroidManifest.xml, add a new ContentProvider:

<provider android:name="com.aware.plugin.template.Provider"
 android:authorities="${applicationId}.provider.plugin.example"
 android:exported="true"
 android:readPermission="com.aware.READ_CONTEXT_DATA"
 android:writePermission="com.aware.WRITE_CONTEXT_DATA"/>
   

Now that we have a ContentProvider, the next steps show how to save data to this database and how to sync the data with the AWARE dashboard (i.e., replicate the data you collect to your study database).

Saving data to the ContentProvider

To save data to your ContentProvider, when you have data to insert into the database, you do as follows:

ContentValues new_data = new ContentValues();
new_data.put(Example_Data.DEVICE_ID, Aware.getSetting(getApplicationContext(), Aware_Preferences.DEVICE_ID));
new_data.put(Example_Data.TIMESTAMP, System.currentTimeMillis());
//put the rest of the columns you defined

//Insert the data to the ContentProvider
getContentResolver().insert(Example_Data.CONTENT_URI, new_data);

Synching data from ContentProvider to dashboard

On your plugin’s Plugin.java class, when you extend the Aware_Plugin class, there are three variables available that you need to assign (DATABASE_TABLESTABLE_FIELDS, and CONTEXT_URIS). Once assigned, AWARE will know that your ContentProvider exists and will sync the data to the server automatically, granted that there are no errors on your database schema.

In your Plugin.java, inside the method onCreate(), we will add this example ContentProvider information, like so:

//To sync data to the server, you'll need to set this variables from your ContentProvider
DATABASE_TABLES = Provider.DATABASE_TABLES;
TABLES_FIELDS = Provider.TABLES_FIELDS;
CONTEXT_URIS = new Uri[]{ Provider.Example_Data.CONTENT_URI };

Next time AWARE tries to sync data to the study database, your plugin’s database will also be synched.

Creating a Context Provider