ContentProvider

In the MyRent series of labs we first used file storage as a means of data persistence. We then replaced this with a database. Here we shall open up this database to other applications using Android's ContentProvider.

Introduction

In this lab we continue from the end of MyRentSQLite, a cut-back version of the MyRent app we have been developing during the past several labs. We have provided an alternative to the file system for data storage in the form of an SQLite database. This added feature, however, is accessible only from within its containing app (MyRentSQLite). Here we shall refactor the app so that its data is accessible not only to the host application but also to other applications installed on the same device.

The Android class ContentProvider constitutes the basic building block for this new feature.

We shall test the new feature in two ways. First we shall verify that if works correctly when exercised by MyRentSQLite. Then we shall develop a very simple Android test app to to read the MyRent database.

The code developed to the end of the previous lab (MyRentSQLite) is available to download: MyRentSQLite

Project organization

Packages

Refactor the sqlite.myrentsqlite packaging as shown in Figure 1.

Figure 1: Refactored packaging

Locate the files as follows:

  • activities: MyRent
  • app: MyRentApp
  • models: Residence
  • providers: DbHelper

At this point both services and cloud packages are empty.

Constants

We shall follow best practice, move the constants from the DbHelper class and locate them in a separate class ResidenceContract located in the providers package.

We have encountered most of the constants in the SQLite lab. The additional constants such as AUTHORITY, CONTENT_URI and so on, relate to implementing ContentProvider.

Much of this module has been influenced by Chapter 11 from Learning Android, 2nd Edition where, for example, detailed explanations are provided on the roles of the various constants. We deal with these also in the accompanying presentation.

package sqlite.myrentsqlite.providers;

import android.net.Uri;
import android.provider.BaseColumns;

/**
 * Created by jfitzgerald on 18/07/2016.
 */
public class ResidenceContract
{
  // Database specific constants

  static final String TAG = "ResidenceContract";
  static final String DATABASE_NAME = "residences.db";
  static final int DATABASE_VERSION = 1;
  static final String TABLE_RESIDENCES = "tableResidences";

  // Provider specific constants
  public static final String AUTHORITY = "sqlite.myrentsqlite.providers.ResidenceProvider";
  public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_RESIDENCES);
  public static final int RESIDENCE_ITEM = 1;
  public static final int RESIDENCE_DIR = 2;
  public static final String STATUS_TYPE_ITEM = "vnd.android.cursor.item/vnd.sqlite.myrentsqlite.providers.provider.status";
  public static final String STATUS_TYPE_DIR = "vnd.android.cursor.dir/vnd.sqlite.myrentsqlite.providers.provider.status";
  public static final String DEFAULT_SORT = Column.DATE + " DESC";

  public class Column
  {
    public static final String ID = BaseColumns._ID;
    public static final String UUID = "uuid";
    public static final String GEOLOCATION = "geolocation";
    public static final String DATE = "date";
    public static final String RENTED = "rented";
    public static final String TENANT = "tenant";
    public static final String ZOOM = "zoom";
    public static final String PHOTO = "photo";
  }

}

DbHelper

Remove all code from DbHelper other than onCreate and onUpgrade.

Here is the refactored class.

package sqlite.myrentsqlite.providers;


import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DbHelper extends SQLiteOpenHelper
{
  static final String TAG = "DbHelper";

  public DbHelper(Context context) {
    super(context, ResidenceContract.DATABASE_NAME, null, ResidenceContract.DATABASE_VERSION);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {

    db.execSQL("CREATE TABLE "
        + ResidenceContract.TABLE_RESIDENCES + " ("
        + ResidenceContract.Column.ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
        + ResidenceContract.Column.UUID + " TEXT,"
        + ResidenceContract.Column.GEOLOCATION + " TEXT,"
        + ResidenceContract.Column.DATE + " TEXT,"
        + ResidenceContract.Column.RENTED + " TEXT,"
        + ResidenceContract.Column.TENANT + " TEXT,"
        + ResidenceContract.Column.ZOOM + " TEXT,"
        + ResidenceContract.Column.PHOTO + " TEXT"
        + ");");
  }

  /**
   * Invoked when schema changed.
   * This determined by comparison existing version and old version.
   *
   * @param db         The SQLite database
   * @param oldVersion The previous database version number.
   * @param newVersion The current database version number.
   */
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    db.execSQL("drop table if exists " + ResidenceContract.TABLE_RESIDENCES);
    Log.d(TAG, "onUpdated");
    onCreate(db);
  }
}

MyRentApp

Previously we maintained an instance of DbHelper in the application class. We will move this to a ContentProvider subclass, dealt with in a future step.

Here is the refactored MyRentApp.

package sqlite.myrentsqlite.app;

import android.app.Application;
import android.util.Log;

public class MyRentApp extends Application
{
  static final String TAG = "MyRentApp";

  private static MyRentApp app;
  @Override
  public void onCreate()
  {
    super.onCreate();
    Log.d(TAG, "MyRent app launched");
    app = this;
  }

  public static MyRentApp getApp(){
    return app;
  }
}

Model

We introduce an overloaded Residence constructor as shown below. Observe that for self-documenting purposes we have changed the name of the UUID field.

package sqlite.myrentsqlite.models;

import java.util.Date;

public class Residence
{
  public UUID uuid;
  public Date date;
  public String geolocation;
  public boolean rented;
  public String tenant;
  public double zoom;//zoom level of accompanying map
  public String photo;


  public Residence()
  {
    uuid = UUID.randomUUID();
    geolocation = "52.253456,-7.187162";
    date = new Date();
    rented = false;
    tenant = ": none presently";
    zoom = 16.0;
    photo = "photo";
  }

  public Residence(String geolocation, boolean rented, String tenant, double zoom, String photo)
  {
    uuid = UUID.randomUUID();
    this.geolocation = geolocation;
    date = new Date();
    this.rented = rented;
    this.tenant = tenant;
    this.zoom = zoom;
    this.photo = photo;
  }
}

ResidenceProvider

This class is the fundamental building block of the new feature.

It will contain a DbHelper field and four key methods:

  1. insert
  2. query
  3. delete
  4. update

We shall develop the application incrementally in the order listed.

Here is providers/ResidenceProvider containing essential boilerplate and the insert method.

package sqlite.myrentsqlite.providers;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

/**
 * Created by jfitzgerald on 18/07/2016.
 * Adapted from code in Learning Android Edition 2
 * Authors: Gargenta & Nakamura
 */
public class ResidenceProvider extends ContentProvider
{
  private static final String TAG = "ResidenceProvider";
  private DbHelper dbHelper;

  private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

  static {
    uriMatcher.addURI(ResidenceContract.AUTHORITY, ResidenceContract.TABLE_RESIDENCES, ResidenceContract.RESIDENCE_DIR);
    uriMatcher.addURI(ResidenceContract.AUTHORITY, ResidenceContract.TABLE_RESIDENCES + "/#", ResidenceContract.RESIDENCE_ITEM);
  }

  @Override
  public boolean onCreate() {

    dbHelper = new DbHelper(getContext());
    Log.d(TAG, "onCreated");
    return true;
  }

  @Nullable
  @Override
  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    return null;
  }

  @Nullable
  @Override
  public String getType(Uri uri) {
    return null;
  }

  @Nullable
  @Override
  public Uri insert(Uri uri, ContentValues values) {
    Uri retUri = null;
    // Assert correct uri
    if (uriMatcher.match(uri) != ResidenceContract.RESIDENCE_DIR) {
      throw new IllegalArgumentException("Illegal uri: " + uri);
    }
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    long rowId = db.insertWithOnConflict(ResidenceContract.TABLE_RESIDENCES, null, values, SQLiteDatabase.CONFLICT_IGNORE);
    // Was insert successful?
    if (rowId != -1) {
      retUri = ContentUris.withAppendedId(uri, rowId);
      Log.d(TAG, "inserted uri: " + retUri);
      // Notify that data for this uri has changed
      getContext().getContentResolver()
          .notifyChange(uri, null);
    }
    return retUri;
  }

  @Override
  public int delete(Uri uri, String selection, String[] selectionArgs) {
    return 0;
  }

  @Override
  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    return 0;
  }

}

RefreshResidenceService

Here we introduce a service. This topic will be the subject of a separate future lab where it will be explained in greater detail.

The class RefreshResidenceService subclasses the Android IntentService. Requests sent to the service are run on a worker thread and routed through onHandleIntent unless onStartCommand is overridden in which case the request is processed on the main thread. To facilitate debugging we shall work on the main thread.

Add this code to the manifest file before the closing </manifest> tag to allow the service to start:

    <service 
      android:name="sqlite.myrentsqlite.services.RefreshResidenceService"
      android:exported="true"/>

Here is the boilerplate service code that includes invoking the provider insert method. Note the presence of a ResidenceCloud object. This is a simple simulation of a cloud service. The code is presented at the end of this page.

package sqlite.myrentsqlite.services;

import android.app.IntentService;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import sqlite.myrentsqlite.cloud.ResidenceCloud;
import sqlite.myrentsqlite.models.Residence;
import sqlite.myrentsqlite.providers.ResidenceContract;

/**
 * Created by jfitzgerald on 18/07/2016.
 */
public class RefreshResidenceService extends IntentService
{
  public static final String TAG = "RefreshResidenceService";
  public static final String REFRESH = "refresh residence";
  public static final String ADD_RESIDENCE = "1";
  public static final String SELECT_RESIDENCE = "2";
  public static final String DELETE_RESIDENCE = "3";
  public static final String SELECT_ALL_RESIDENCES = "4";
  public static final String DELETE_RESIDENCES = "5";
  public static final String UPDATE_RESIDENCE = "6";

  ResidenceCloud cloud = new ResidenceCloud();

  /**
   * Zero argument constructor required
   */
  public RefreshResidenceService() {
    super("RefreshResidenceService");
  }

  /**
   * @param name Used to name the worker thread, important only for debugging.
   */
  public RefreshResidenceService(String name) {
    super(name);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    String value = intent.getStringExtra(REFRESH);
    switch (value) {

      case ADD_RESIDENCE:
        addResidence(new Residence());
        break;
    }

    return START_STICKY;
  }

  private void addResidence(Residence residence) {
    ContentValues values = new ContentValues();

    values.put(ResidenceContract.Column.UUID, residence.uuid.toString());
    values.put(ResidenceContract.Column.GEOLOCATION, residence.geolocation);
    values.put(ResidenceContract.Column.DATE, String.valueOf(residence.date.getTime()));
    values.put(ResidenceContract.Column.RENTED, residence.rented == true ? "yes" : "no");
    values.put(ResidenceContract.Column.TENANT, residence.tenant);
    values.put(ResidenceContract.Column.ZOOM, Double.toString(residence.zoom));
    values.put(ResidenceContract.Column.PHOTO, residence.photo);

    Uri uri = getContentResolver().insert(
        ResidenceContract.CONTENT_URI, values);
  }

  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "onHandleIntent invoked");
  }

}

Create a package sqlite.myrentsqlite.cloud. This is home to a simulated cloud service. This uses the newly added model Residence overloaded constructor. Create a class ResidenceCloud with the following content:

package sqlite.myrentsqlite.cloud;

import java.util.ArrayList;
import java.util.List;

import sqlite.myrentsqlite.models.Residence;

/**
 * Created by jfitzgerald on 18/07/2016.
 * Simulated cloud
 */
public class ResidenceCloud
{
  public static Residence residence() {
    return new Residence("52.4444,-7.187162", true, "Barney Gumble", 12.0, "photo1.jpeg");
  }

  public static List<Residence> residences() {
    ArrayList<Residence> list = new ArrayList<>();

    list.add(new Residence("52.4444,-7.187162", true, "Barney Gumble", 12.0, "photo1.jpeg"));
    list.add(new Residence("52.3333,-7.187162", true, "Ned Flanders", 16.0, "photo2.jpeg"));

    return list;
  }

}

Layout and Activity

We shall use adapt the MyRent activity from the previous lab. The layout remains the same other than the elimination of the Get RowId.

Here is the MyRent activity code. Previous references to RowId have been deleted. The event handler methods have been retained with empty bodies as placeholders. The exception is the addResidence method where the RefreshResidenceService is started and configured to insert the Residence record into the SQLite database:

Filename: activities/MyRent.java

package sqlite.myrentsqlite.activities;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import sqlite.myrentsqlite.R;
import sqlite.myrentsqlite.services.RefreshResidenceService;;


public class MyRent extends AppCompatActivity implements View.OnClickListener
{

  private Button addResidence;
  private Button selectResidence;
  private Button deleteResidence;
  private Button selectAllResidences;
  private Button deleteAllResidences;
  private Button updateResidence;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_myrent);

    addResidence = (Button) findViewById(R.id.addResidence);
    addResidence.setOnClickListener(this);

    selectResidence = (Button) findViewById(R.id.selectResidence);
    selectResidence.setOnClickListener(this);

    deleteResidence = (Button) findViewById(R.id.deleteResidence);
    deleteResidence.setOnClickListener(this);

    selectAllResidences = (Button) findViewById(R.id.selectAllResidences);
    selectAllResidences.setOnClickListener(this);

    deleteAllResidences = (Button) findViewById(R.id.deleteAllResidences);
    deleteAllResidences.setOnClickListener(this);

    updateResidence = (Button) findViewById(R.id.updateResidence);
    updateResidence.setOnClickListener(this);

  }

  @Override
  public void onClick(View v) {
    switch (v.getId()) {
      case R.id.addResidence:
        addResidence();
        break;

      case R.id.selectResidence:
        selectResidence();
        break;

      case R.id.deleteResidence:
        deleteResidence();
        break;

      case R.id.selectAllResidences:
        selectAllResidences();
        break;

      case R.id.deleteAllResidences:
        deleteAllResidences();
        break;

      case R.id.updateResidence:
        updateResidence();
        break;

    }
  }

  /**
   * Start a RefreshResidence service, passing the intent message ADD_RESIDENCE
   * This determines what functionality is invoked in the service.
   */
  private void addResidence() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.ADD_RESIDENCE);
    startService(intent);
  }

  /**
   * Select a single Residence record
   */
  public void selectResidence() {

  }

  /**
   * Select all Residence records
   */
  public void selectAllResidences() {

  }

  public void deleteResidence() {

  }

  /**
   * Delete all records.
   */
  public void deleteAllResidences() {

  }

  /**
   * Update a residence record.
   */
  public void updateResidence() {

  }

}

Before testing, ensure that the manifest in MySQLite has set the permission to allow its database access from another app. This is achieved by adding an attribute android:exported="true" to the provider node as shown here:

...
...
    <provider
      android:name="sqlite.myrentsqlite.providers.ResidenceProvider"
      android:authorities="sqlite.myrentsqlite.providers.ResidenceProvider"
      android:exported="true"/>

  </application>

And most importantly, declare the service in the manifest. Without doing so the service will not start and no warning will be provided.

    <service android:name="sqlite.myrentsqlite.services.RefreshResidenceService"/>

Build and run the application. Press the Add Residence button. Create an appropriate log filter, for example as shown in Figure 2. Observe the output on the logcat window. If should include something like that shown in Figure 1.

Figure 1: LogCat output

Check the database content using the adb shell command.

Figure 2: Log filter to observe ResidenceProvider output

query

We have fully implemented insert in the previous steps. Here we implement the query functionality.

Activity


  /**
   * Select a single Residence record
   */
  public void selectResidence() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.SELECT_RESIDENCE);
    startService(intent);
  }


  /**
   * Select all Residence records
   */
  public void selectAllResidences() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.SELECT_ALL_RESIDENCES);
    startService(intent);
  }

Provider

/**
   * Refer to page 195 Learning Android 2nd Edition for more detailed informtion.
   * Also Android SQLite Database (wish the url not so long).
   * And Vogella: http://www.vogella.com/tutorials/AndroidSQLite/article.html#base-uri-of-the-content-provider
   *
   * @param uri Universal Resource Identifier
   * @param projection The set of columns to be returned - null to return all
   * @param selection Filter to specify which rows to return - null to return all
   * @param selectionArgs Replace ?s in selection by strings
   * @param sortOrder How to sort rows - null for default sort
   * @return
   */
  @Nullable
  @Override
  public Cursor query(Uri uri, String[] projection, String selection,
                      String[] selectionArgs, String sortOrder) {
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    qb.setTables( ResidenceContract.TABLE_RESIDENCES ); // Specify table
    switch (uriMatcher.match(uri)) { // Determine uri type
      case ResidenceContract.RESIDENCE_DIR:
        break;
      case ResidenceContract.RESIDENCE_ITEM: // uri contains record id
        qb.appendWhere(ResidenceContract.Column.ID + "=" + uri.getLastPathSegment());
        break;
      default:        
        throw new IllegalArgumentException("Illegal uri: " + uri);
    }

    String orderBy = (TextUtils.isEmpty(sortOrder))
        ? ResidenceContract.DEFAULT_SORT
        : sortOrder; // Specify sort order of returned data

    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor cursor = qb.query(db, projection, selection, selectionArgs,
        null, null, orderBy);

    // register for uri changes
    cursor.setNotificationUri(getContext().getContentResolver(), uri); // Cursor data refresh
    Log.d(TAG, "queried records: " + cursor.getCount());
    return cursor;
  }

Service

Add to the switch statement in onStartCommand:

      case SELECT_RESIDENCE:
        selectResidence();
        break;

      case SELECT_ALL_RESIDENCES:
        selectAllResidences();
        break;

Now add the selection functionality:

  /**
   * Select a single residence. 
   * To ensure the database is populated
   * we first create a default Residence object and add it to the database.
   */
  private void selectResidence() {
    Residence residence = ResidenceCloud.residence();
    addResidence(residence);
    selectResidence(residence.uuid);
  }
  /**
   * Test query method in ResidenceProvider by
   * obtaining a single residences from simulated cloud,
   * adding this residence as record to database,
   * querying database for this record and
   * checking result
   * Refer to ResidenceProvider.query for documentation query params
   */
  private Residence selectResidence(UUID uuid) {
    Residence residence = new Residence();
    String selection = ResidenceContract.Column.UUID + " = ?";
    String[] selectionArgs = new String[]{uuid + ""};

    // Query database
    Cursor cursor = getContentResolver().query(ResidenceContract.CONTENT_URI, null, selection, selectionArgs, null);

    if (cursor.getCount() > 0) {
      int columnIndex = 1; // Skip the 0th column - the _id
      cursor.moveToFirst();

      residence.uuid = UUID.fromString(cursor.getString(columnIndex++));
      residence.geolocation = cursor.getString(columnIndex++);
      residence.date = new Date(Long.parseLong(cursor.getString(columnIndex++)));
      residence.rented = cursor.getString(columnIndex++) == "yes" ? true : false;
      residence.tenant = cursor.getString(columnIndex++);
      residence.zoom = Double.parseDouble(cursor.getString(columnIndex++));
      residence.photo = cursor.getString(columnIndex++);

    }
    cursor.close();

    return residence;
  }

  /**
   * Test query method in ResidenceProvider by
   * obtaining a list of residences from simulated cloud,
   * adding each residence as record to database,
   * querying database for this list and
   * checking result
   */
  private void selectAllResidences() {
    populateSampleData();

    // Query the database
    List<Residence> residences = new ArrayList<Residence>();
    Cursor cursor = getContentResolver().query(ResidenceContract.CONTENT_URI, null, null, null, null);

    if (cursor.moveToFirst()) {
      int columnIndex = 1; // skip column 0, the _id
      do {
        Residence residence = new Residence();

        residence.uuid = UUID.fromString(cursor.getString(columnIndex++));
        residence.geolocation = cursor.getString(columnIndex++);
        residence.date = new Date(Long.parseLong(cursor.getString(columnIndex++)));
        residence.rented = cursor.getString(columnIndex++) == "yes" ? true : false;
        residence.tenant = cursor.getString(columnIndex++);
        residence.zoom = Double.parseDouble(cursor.getString(columnIndex++));
        residence.photo = cursor.getString(columnIndex++);

        columnIndex = 1;

        residences.add(residence);
      } while (cursor.moveToNext());
    }
    cursor.close();

  }

  /**
   * Populate database with list sample residences
   * Used for testing
   */
  private List<Residence> populateSampleData() {
    List<Residence> residenceList = ResidenceCloud.residences();
    for (Residence residence : residenceList) {
      addResidence(residence);
    }
    return residenceList;
  }

Build, run and test by clicking on first, SELECT RESIDENCE and then SELECT ALL buttons while observing output in the logcat pane.

Then examine database content using adb shell.

delete

Activity

  /**
   * Delete a single Residence record
   */
  public void deleteResidence() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.DELETE_RESIDENCE);
    startService(intent);
  }

  /**
   * Delete all records.
   */
  public void deleteAllResidences() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.DELETE_RESIDENCES);
    startService(intent);
  }

Provider

The delete method:

  @Override
  public int delete(Uri uri, String selection, String[] selectionArgs) {
    String where;
    switch (uriMatcher.match(uri)) {
      case ResidenceContract.RESIDENCE_DIR:
        // so we count deleted rows
        where = (selection == null) ? "1" : selection;
        break;
      case ResidenceContract.RESIDENCE_ITEM:
        long id = ContentUris.parseId(uri);
        where = ResidenceContract.Column.ID
            + "="
            + id
            + (TextUtils.isEmpty(selection) ? "" : " and ( "
            + selection + " )");
        break;
      default:
        throw new IllegalArgumentException("Illegal uri: " + uri);
    }
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    int ret = db.delete(ResidenceContract.TABLE_RESIDENCES, where, selectionArgs);
    if(ret > 0) {
      // Notify that data for this uri has changed
      getContext().getContentResolver().notifyChange(uri, null);
    }
    Log.d(TAG, "deleted records: " + ret);
    return ret;
  }

Service

Add to the switch statement in onStartCommand:

      case DELETE_RESIDENCE:
        deleteResidence();
        break;

      case DELETE_RESIDENCES:
        deleteAllResidences();
        break;

Here are the delete methods:

  /**
   * Add a list of residences to database
   * Pick on at random from the list and delete it from db
   */
  private void deleteResidence() {
    List<Residence> residenceList = populateSampleData();

    String uuid = residenceList.get(0).uuid.toString(); // Pick the first row (arbitrarily)
    String selection = ResidenceContract.Column.UUID + " = ?";
    String[] selectionArgs = new String[]{uuid + ""};
    int response = getContentResolver().delete(ResidenceContract.CONTENT_URI, selection, selectionArgs);
    Log.d(TAG, "delete record response: " + response);
  }

  /**
   * Delete all Residence records.
   */
  private void deleteAllResidences() {
    List<Residence> residenceList = populateSampleData();

    int response = getContentResolver().delete(ResidenceContract.CONTENT_URI, null, null);
    Log.d(TAG, "delete all records response: " + response);
  }

Build, run and test by clicking on first, DELETE RESIDENCE and then DELETE ALL buttons while observing output in the logcat pane.

Then examine database content using adb shell.

update

Activity

  /**
   * Update a residence record.
   */
  public void updateResidence() {
    Intent intent = new Intent(getBaseContext(), RefreshResidenceService.class);
    intent.putExtra(RefreshResidenceService.REFRESH, RefreshResidenceService.UPDATE_RESIDENCE);
    startService(intent);
  }

Provider

  @Override
  public int update(Uri uri, ContentValues values, String selection,
                    String[] selectionArgs) {
    String where;
    switch (uriMatcher.match(uri)) {
      case ResidenceContract.RESIDENCE_DIR:
        // so we count updated rows
        where = selection; 
        break;
      case ResidenceContract.RESIDENCE_ITEM:
        long id = ContentUris.parseId(uri);
        where = ResidenceContract.Column.ID
            + "="
            + id
            + (TextUtils.isEmpty(selection) ? "" : " and ( "
            + selection + " )");
        break;
      default:
        throw new IllegalArgumentException("Illegal uri: " + uri);
    }
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    int ret = db.update(ResidenceContract.TABLE_RESIDENCES, values,
        where, selectionArgs);
    if(ret > 0) {
      // Notify that data for this URI has changed
      getContext().getContentResolver().notifyChange(uri, null);
    }
    Log.d(TAG, "updated records: " + ret);
    return ret;
  }

Service

Add to the switch statement in onStartCommand:

      case UPDATE_RESIDENCE:
        updateResidence();
        break;
  private void updateResidence() {
    // Populate database with sample residence
    Residence residence = ResidenceCloud.residence();
    addResidence(residence);

    // Modify the residence and update database copy
    residence.zoom = 40;
    residence.tenant = "A. N. Other";

    updateResidence(residence);

    Residence residenceUpdated = selectResidence(residence.uuid);

    boolean testResult = residence.zoom == residenceUpdated.zoom &&
        residence.tenant.equals(residenceUpdated.tenant);

    Log.d(TAG, "update residence attempt: " + testResult);
  }
  /**
   * Update a residence record
   */
  private void updateResidence(Residence residence) {
    String uuid = residence.uuid.toString();
    String selection = ResidenceContract.Column.UUID + " = ?";
    String[] selectionArgs = new String[]{uuid + ""};

      ContentValues values = new ContentValues();

      values.put(ResidenceContract.Column.GEOLOCATION, residence.geolocation);
      values.put(ResidenceContract.Column.DATE, String.valueOf(residence.date.getTime()));
      values.put(ResidenceContract.Column.RENTED, residence.rented == true ? "yes" : "no");
      values.put(ResidenceContract.Column.TENANT, residence.tenant);
      values.put(ResidenceContract.Column.ZOOM, Double.toString(residence.zoom));
      values.put(ResidenceContract.Column.PHOTO, residence.photo);

      getContentResolver().update(ResidenceContract.CONTENT_URI, values, selection, selectionArgs);

    }

Build, run and test by clicking on the UPDATE button while observing output in the logcat pane.

Then examine database content using adb shell to verify that the changed fields have been persisted.

The next lab will access this database from another application using the newly developed ContentProvider interface.

Test

Here we describe how to access the MyRentSQlite generated database from a different app. Both apps are deployed on the same device.

Create a new Android application named ContentProviderTest, accepting the wizard's defaults but, for compatibility with code samples below, define the company domain as ictskills.

Create a project package structure similar to that here:

Figure 1: Recommended package structure

Model

Here is the model Residence class. Its fields match the MyRentSQLite model.

package ictskills.contentprovidertest.models;
import java.util.Date;

public class Residence
{
  public UUID uuid;
  public Date date;
  public String geolocation;
  public boolean rented;
  public String tenant;
  public double zoom;
  public String photo;

  public Residence() { }
}

Layout

The text file:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="ictskills.contentprovidertest.activities.MainActivity">

  <Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Access MyRent Content Provider"
      android:id="@+id/contentProvider"
      android:layout_alignParentTop="true"
      android:layout_alignParentStart="true"
      android:layout_marginStart="33dp"
      android:layout_marginTop="89dp"/>
</RelativeLayout>

Figure 2: ContentProviderTest UI

Activity

In the only activity, MainActivity, we add:

  • A button handler. A button click invokes a method that reads the SQLite database in the MyRentSQLite app.
  • A method, selectAllResidences, that populates a locally created Residence list with the content of MyRentSQLite's database. (Ensure that you run MyRentSQLite and add some Residence records before this method is invoked).
package ictskills.contentprovidertest.activities;

import android.database.Cursor;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import ictskills.contentprovidertest.R;
import ictskills.contentprovidertest.models.Residence;
import ictskills.contentprovidertest.providers.ResidenceContract;

import android.widget.Toast;

/**
 * Set a button click handler.
 * Handler obtains list Residence records from MyRentSqlite
 * Precondition: MyRentSqlite database must be present.
 * Install and run MyRentSqlite and add some Residence records to populate the database.
 *
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener
{
  private Button contentProvider;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    contentProvider = (Button)findViewById(R.id.contentProvider);
    contentProvider.setOnClickListener(this);
  }

  @Override
  public void onClick(View v) {
    switch(v.getId()) {
      case R.id.contentProvider:
        selectAllResidences();
    }
  }

  /**
   * This code derived from MyRentSQLite.RefreshResidenceService.selectAllResidences.
   */
  private void selectAllResidences() {

    // Query the database
    List<Residence> residences = new ArrayList<Residence>();
    Cursor cursor = getContentResolver().query(ResidenceContract.CONTENT_URI, null, null, null, null);

    if (cursor.moveToFirst()) {
      int columnIndex = 1; // skip column 0, the _id
      do {
        Residence residence = new Residence();

        residence.uuid = UUID.fromString(cursor.getString(columnIndex++));
        residence.geolocation = cursor.getString(columnIndex++);
        residence.date = new Date(Long.parseLong(cursor.getString(columnIndex++)));
        residence.rented = cursor.getString(columnIndex++) == "yes" ? true : false;
        residence.tenant = cursor.getString(columnIndex++);
        residence.zoom = Double.parseDouble(cursor.getString(columnIndex++));
        residence.photo = cursor.getString(columnIndex++);

        columnIndex = 1;

        residences.add(residence);
      } while (cursor.moveToNext());
    }
    cursor.close();
    Toast.makeText(this, "Retrieved MyRentSQLite Residence array, size: " + residences.size(), Toast.LENGTH_SHORT).show();
  }
}

Build and deploy the app to a device or emulator on which MyRentSQLite is present and whose database is populated.

Press the ACCESS MYRENT CONTENT PROVIDER button.

If the app successfully retrieves the database you should see a Toast message somewhat similar to that shown in Figure 3.

Figure 3: Toast message indicating that 11 records retrieved

The application at the end of this lab is available for reference here: sqlite_content_provider. The content provider is on a branch named contentprovider_complete.

Also available is the application to test the content provider: sqlite_content_provider_test.