Android ContentProvider tutorial

The Android framework uses a concept called content providers to enable applications to share and use data across the platform. A content provider is an owner of particular content, it provides well defined APIs to read, insert, update and delete that data. The content provider can internally use any place to store its data like a local file, local database or some remote service.

A content provider component supplies data from one application to others on request. Such requests are handled by the methods of the ContentResolver class. A content provider can use different ways to store its data and the data can be stored in a database, in files, or even over a network.

android_content_provider.jpg

Content providers let you centralize content in one place and have many different applications access it as needed. A content provider behaves very much like a database where you can query it, edit its content, as well as add or delete content using insert(), update(), delete(), and query() methods. In most cases this data is stored in an SQlite database.

A content provider is implemented as a subclass of ContentProvider class and must implement a standard set of APIs that enable other applications to perform transactions.

public class MyContentProvider extends ContentProvider {
}

Content providers create an abstraction layer between its repository of data and external application that are using data. External Application can call ContentProvider methods with the use of ContentResolver. ContentResolver work as ContentProvider client object, with the use of ContentResolver object we can get data from ContentProvider.

To query a content provider, you specify the query string in the form of a URI which has following format

<prefix>://<authority>/<data_type>/<id>

Here are the details of various parts of the URI

  • prefix is always set to content://
  • authority specifies the name of the content provider, for example contacts, browser etc. For third-party content providers, this could be the fully qualified name, such as me.proft.MovieProvider.
  • data_type indicates the type of data that this particular provider provides. For example, if you are getting all the contacts from the Contacts content provider, then the data path would be people and URI would look like this content://contacts/people.
  • id specifies the specific record requested. For example, if you are looking for contact number 5 in the Contacts content provider then URI would look like this content://contacts/people/5.

A ContentProvider usually has 3 components, a constants class that defines the schema for your tables and your content authority, an SQLite helper class that physically creates the table, and the provider class, in which you implement the URiMatcher and your CRUD methods.

Creating of ContentProvider involves number of simple steps to create your own content provider.

  1. Create a ContentProvider class that extends the ContentProvider base class.
  2. You need to define your content provider URI address which will be used to access the content.
  3. Next you will need to create your own database to keep the content. Usually, Android uses SQLite database and framework needs to override onCreate() method which will use SQLite Open Helper method to create or open the provider's database. When your application is launched, the onCreate() handler of each of its content providers is called on the main application thread.
  4. Next you will have to implement ContentProvider queries to perform different database specific operations.
  5. Finally register your ContentProvider in your activity file using <provider> tag.

Here is the list of methods which you need to override in ContentProvider class to have your ContentProvider working.

android_content_provider_ops.png
  • onCreate() method is called when the provider is started.
  • query() method receives a request from a client. The result is returned as a Cursor object.
  • insert() method inserts a new record into the content provider.
  • delete() method deletes an existing record from the content provider.
  • update() method updates an existing record from the content provider.
  • getType() method returns the MIME type of the data at the given URI.

To retrieve the data, we need to follow these step

  • Request the read access permission for the provider
  • Construct the query and send it to provide

Following example will explain you how to create your own ContentProvider.

First, modify main activity file MainActivity.java to add two new methods addMovie() and getMovies().

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    public void addMovie(View view) {
        // add a new movie record

        String title = ((EditText)findViewById(R.id.etTitle)).getText().toString();
        int year = Integer.valueOf(((EditText)findViewById(R.id.etYear)).getText().toString());

        ContentValues values = new ContentValues();

        values.put(MovieProvider.TITLE, title);
        values.put(MovieProvider.YEAR, year);

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

        Toast.makeText(getBaseContext(), uri.toString(), Toast.LENGTH_LONG).show();
    }

    public void getMovies(View view) {
        // retrieve movie records

        String URL = "content://" + MovieProvider.PROVIDER_NAME;
        Uri moviesUri = Uri.parse(URL);
        String orderBy = MovieProvider.TITLE;
        Cursor c = getContentResolver().query(moviesUri, null, null, null, orderBy);

        if (c.moveToFirst()) {
            String id, title, year;
            do {
                id = c.getString(c.getColumnIndex(MovieProvider.ID));
                title = c.getString(c.getColumnIndex(MovieProvider.TITLE));
                year = c.getString(c.getColumnIndex(MovieProvider.YEAR));
                Toast.makeText(this, id + ", " + title + ", " + year, Toast.LENGTH_SHORT).show();
            } while (c.moveToNext());
        }
    }
}

Create a new java file called MovieProvider.java to define your actual provider and associated methods. Inside of our MovieProvider class, we need to define a few properties: a content authority (PROVIDER_NAME), which is a unique identifier for our database and a base URI (CONTENT_URI).

public class MovieProvider extends ContentProvider {
    static final String PROVIDER_NAME = "me.proft.MovieProvider";
    static final String URL = "content://" + PROVIDER_NAME + "/movies";
    static final Uri CONTENT_URI = Uri.parse(URL);

    static final String ID = "id";
    static final String TITLE = "title";
    static final String YEAR = "year";

    private static HashMap<String, String> MOVIES_PROJECTION_MAP;

    static final int MOVIES = 1;
    static final int MOVIE_ID = 2;

    static final UriMatcher uriMatcher;

    static{
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(PROVIDER_NAME, "movies", MOVIES);
        uriMatcher.addURI(PROVIDER_NAME, "movies/#", MOVIE_ID);
    }

    // database specific constant declarations

    private SQLiteDatabase db;
    static final String DATABASE_NAME = "Movie";
    static final String TABLE_NAME = "movies";
    static final int DATABASE_VERSION = 1;

    static final String CREATE_DB_TABLE =
        " CREATE TABLE " + TABLE_NAME +
                " (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                " title TEXT NOT NULL, " +
                " year INTEGER NOT NULL);";

    // helper class that actually creates and manages the provider's underlying data repository.

    private static class DatabaseHelper extends SQLiteOpenHelper {
        DatabaseHelper(Context context){
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(CREATE_DB_TABLE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            db.execSQL("DROP TABLE IF EXISTS " +  TABLE_NAME);
            onCreate(db);
        }
    }

    @Override
    public boolean onCreate() {
        Context context = getContext();
        DatabaseHelper dbHelper = new DatabaseHelper(context);

        // create a write able database which will trigger its creation if it doesn't already exist.

        db = dbHelper.getWritableDatabase();
        return (db == null)? false:true;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // add a new movie record
        long rowID = db.insert(TABLE_NAME, "", values);

        // if record is added successfully
        if (rowID > 0) {
            Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
            getContext().getContentResolver().notifyChange(_uri, null);
            return _uri;
        }

        throw new SQLException("Failed to add a record into " + uri);
    }

    @Override
    public Cursor query(Uri uri, String[] projection,
                        String selection, String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(TABLE_NAME);

        switch (uriMatcher.match(uri)) {
            case MOVIES:
                qb.setProjectionMap(MOVIES_PROJECTION_MAP);
                break;

            case MOVIE_ID:
                qb.appendWhere(ID + "=" + uri.getPathSegments().get(1));
                break;

            default:
        }

        if (sortOrder == null || sortOrder == ""){
            // by default sort on movie title
            sortOrder = TITLE;
        }

        Cursor c = qb.query(db,   projection, selection,
                selectionArgs,null, null, sortOrder);
        // register to watch a content URI for changes
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
        switch (uriMatcher.match(uri)){
            case MOVIES:
                count = db.delete(TABLE_NAME, selection, selectionArgs);
                break;

            case MOVIE_ID:
                String id = uri.getPathSegments().get(1);
                count = db.delete(TABLE_NAME, ID +  " = " + id + (!TextUtils.isEmpty(selection) ? " 
                            AND (" + selection + ')' : ""), selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values,
                      String selection, String[] selectionArgs) {
        int count = 0;
        switch (uriMatcher.match(uri)) {
            case MOVIES:
                count = db.update(TABLE_NAME, values, selection, selectionArgs);
                break;

            case MOVIE_ID:
                count = db.update(TABLE_NAME, values, ID + " = " + uri.getPathSegments().get(1) + 
                            (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""), selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI " + uri );
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            // get all movie records
            case MOVIES:
                return "vnd.android.cursor.dir/vnd.me.proft.movies";
            //  get a particular student
            case MOVIE_ID:
                return "vnd.android.cursor.item/vnd.me.proft.movies";
            default:
                throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
    }
}

To deal with multiple URIs Android provides the helper class UriMatcher. This class eases the parsing of URIs.

The query() method takes in five parameters:

  • uri. The URI (or table) that should be queried.
  • projection. A string array of columns that will be returned in the result set.
  • selection. A string defining the criteria for results to be returned.
  • selectionArgs. Arguments to the above criteria that rows will be checked against.
  • sortOrder. A string of the column(s) and order to sort the result set by.

The insert() method takes in a ContentValues object, which is a key value pair of column names and values to be inserted.

The update() and delete() methods take in a selection string and arguments to define which rows should be updated or deleted. They differ in that the update method requires a ContentProvider object as well, for the columns in that row(s) that will be updated.

The getType method is used to find the MIME type of the results, either a directory of multiple results, or an individual item.

Register your content provider in your AndroidManifest.xml file using tag

<manifest ...
   <application ...

      <provider android:name="MovieProvider" android:authorities="me.proft.MovieProvider"/>
   </application>
</manifest>

Modify the default content of activity_main.xml file.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    android:padding="2dp">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Title"
        android:id="@+id/etTitle"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Year"
        android:id="@+id/etYear"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btnAdd"
        android:text="Add movie"
        android:onClick="addMovie"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btnGetMovie"
        android:text="Get movie"
        android:onClick="getMovies"/>
</LinearLayout>

Run the application to launch Android emulator and verify the result of the changes done in the application.

android_content_provider_example.png
comments powered by Disqus