Content Provider Framework

Stoyan Rachev edited this page Jun 24, 2012 · 1 revision
Clone this wiki locally

Introduction

Feeder's Content Provider Framework enables you to create a fully functional Android content provider with much less code than when using the standard approach. Behind the scenes, the framework uses database functionality and metadata information provided by ORMLite and a few additional custom annotations to build content queries and manage database tables in a generic way.

At the moment, this framework is part of the Feeder application, so it cannot be readily used in other applications. If there is enough interest, I will extract it into its own open source project, or integrate it into an existing one such as android-annotations.

This article assumes that you are familiar with object-relational mapping (ORM) in general, as well as with the basic principles of building Android content providers with SQLite. See Android SQLite Database and ContentProvider Tutorial for background information on this topic.

Tutorial

Creating The Data Model Classes

The data model classes represent entities from your problem domain. In Feeder, these are channels (feeds) and items (feed entries). When using ORMLite, you map your entity classes to database tables by annotating them either with native ORMLite annotations (@DatabaseTable, @DatabaseField), or with JPA annotations (@Entity, etc.).

In addition, you can use several additional annotations to enrich your entities with additional information used by our framework:

  • The @UriPaths annotation takes as a parameter a list of UriMatcher URI patterns. Whenever a content provider configured with our entity is asked to perform an operation for a matching URI, it queries or updates the corresponding database table in an appropriate way.
  • The @MimeType annotation specifies the second part of the MIME type returned by the content provider getType(Uri) method whenever the passed URI matches one of the patterns specified with the previous annotation.
  • The @DefaultSortOrder annotation specifies the default sort order used by the content provider when performing queries for a matching URI.

Here is an example for using these annotations in our Channel entity:

@DatabaseTable
@UriPaths({ "channels", "channels/#" })
@MimeType("vnd.feeder.channel")
@DefaultSortOrder(Channel.TITLE + " ASC")
public class Channel extends Data {

    public static final String TITLE = "title";
    ...

    @DatabaseField(columnName = TITLE, unique = true, canBeNull = false)
    private String title;
    ... 

Note that the Channel class extends the Data class, which is also part of our framework and represents a base entity. It has a single @DatabaseField-annotated field id for the mandatory auto-generated primary key id column.

See:

Creating The Content Provider

Once we have our model classes, creating a fully functional content provider for them is a simple matter of extending our AbstractContentProvider and overriding the onCreate() method to configure it appropriately:

public class MainContentProvider extends AbstractContentProvider {
    public static final String AUTHORITY = "com.stoyanr.feeder.content";
    private static final Class[] CLASSES = { Channel.class, Item.class };

    @Override
    public boolean onCreate() {
        super.setHelper(new DatabaseHelper(CLASSES, "feeder.db", 10, getContext()));
        super.setAuthority(AUTHORITY);
        super.initialize(CLASSES);
        return true;
    }
}

Yes, this is virtually all code you need to write. If you have written Android content providers in the standard way, you should be missing here a few hundred lines more of boilerplate code. All this is largely replaced by the functionality available in the framework AbstractContentProvider and DatabaseHelper classes.

See:

Using The Content Provider In Your Activities

You use the above content provider in your activities much in the same way you would use any other content provider. If you are using the recommended Loaders approach for loading data asynchronously, you would write code that is similar to the following:

    private final LoaderManager.LoaderCallbacks<Cursor> clc = new LoaderManager.LoaderCallbacks<Cursor>() {

        private static final String[] PROJECTION = new String[] { Channel._ID,
            Channel.ICON, Channel.TITLE, Channel.URL, Channel.IMAGE };

        @Override
        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
            return new CursorLoader(ChannelsActivity.this, getIntent().getData(), 
                PROJECTION, null, null, null);
        }

        @Override
        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
            adapter.changeCursor(cursor);
        }
        
        ...

In the above code, the onCreateLoader() method creates a new CursorLoader by using the appropriate URI and projection. The URI obtained via a call to getIntent().getData() matches one of the patterns specified in the Channel entity via the @UriPaths annotation, as declared for our activity in the Android manifest. The onLoadFinished method simply passes the obtained cursor to our custom adapter for the data binding mechanism to do its magic.

See:

Creating And Using A Content Manager

On more than a few occasions, you might need to perform certain data operations besides simply loading it in cursors for data binding. A content manager or repository is a convenient facade for performing operations on data. It hides the actual data access mechanism from the other parts of your application. For example, in order to query a single channel by its id, you would write the following code in one of your activities:

    private ContentManager cm;

    @Override
    protected void onCreate(Bundle bundle) {
        ...
        cm = new ContentManager(getContentResolver());
    }
    
    ...
    
    private Channel getChannel(long id) {
        return cm.queryChannelById(id);
    }

When implementing the content manager, you have the choice of using either the content provider or the database helper to perform the actual operations. Using the database helper however is much more convenient due to the functionality provided by ORMLite and the framework DatabaseHelper class.

public final class ContentManager {

    private final DatabaseHelper helper;

    public ContentManager(ContentResolver cr) {
        AbstractContentProvider provider = (AbstractContentProvider) cr
            .acquireContentProviderClient(MainContentProvider.AUTHORITY)
            .getLocalContentProvider();
        this.helper = provider.getHelper();
    }

    public Channel queryChannelById(long id) {
        return helper.queryById(id, Channel.class);
    }
    
    ...

In the above snippet, the content manager obtains the database helper via the content provider in its constructor, and then uses it to perform operations on data in a very convenient and straightforward way.

See:

Behind The Scenes

Classes Overview

Class Diagram

The DatabaseHelper Class

The DatabaseHelper class extends the standard OrmLiteSqliteOpenHelper provided by ORMLite as recommended in the ORMLite documentation. It is configured by simply passing an array containing all your entity classes. Internally, it manages a map of Dao<? extends Data, Long> objects for each entity class, which are used to perform the actual database operations.

class DatabaseHelper extends OrmLiteSqliteOpenHelper {

    private final Class<? extends Data>[] classes;
    private final Map<Class<? extends Data>, Dao<? extends Data, Long>> daos = 
        new HashMap<Class<? extends Data>, Dao<? extends Data, Long>>();

    public DatabaseHelper(Class<? extends Data>[] classes, String name,
        int version, Context ctx) {
        super(ctx, name, null, version);
        this.classes = classes;
    }

    @Override
    public void onCreate(SQLiteDatabase db, ConnectionSource cs) {
        ...
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, ConnectionSource cs,
        ...
    }

    @Override
    public void close() {
        super.close();
        daos.clear();
    }

    public <T extends Data> Dao<T, Long> getDaoEx(Class<T> clazz) {
        Dao<T, Long> result = null;
        if (daos.containsKey(clazz)) {
            result = (Dao<T, Long>) daos.get(clazz);
        } else {
            result = getDao(clazz);
            daos.put(clazz, result);
        }
        return result;
    }

    public <T extends Data> T queryById(long id, Class<T> clazz) {
        return getDaoEx(clazz).queryForId(id);
    }
    
    ...

In the above snippet, the methods onCreate() and onUpgrade() use standard ORMLite functionality to create or upgrade the database tables. Note that the default onUpgrade() implementation drops and then re-creates all database tables. You would need to extend this class and override this method if you would like to do a real upgrade.

See:

The AbstractContentProvider Class

The AbstractContentProvider class is the heart of our framework. It provides standard implementations of all important content provider methods such as getType(), query(), insert(), update(), delete(), and shutdown(). Besides methods for setting the authority and the database helper, it provides also the initialize() method which accepts an array containing a list of entity classes. Behind the scenes, it uses standard ORMLite functionality available via the database helper, as well as the information provided in the annotations of the passed entity classes to build the actual database queries.

public abstract class AbstractContentProvider extends ContentProvider {

    private String authority;
    private DatabaseHelper helper;

    private UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    ...

    protected void initialize(Class<? extends Data>[] classes) {
        int code = 1;
        for (Class<? extends Data> clazz : classes) {
            for (String path : getUriPaths(clazz)) {
                matcher.addURI(authority, path, code);
                ...
                code++;
            }
        }
    }
    
    ...

    @Override
    public Cursor query(Uri url, String[] projection, String selection,
        String[] selectionArgs, String sort) {
        int code = matcher.match(url);
        if (!isValidCode(code))
            throw new IllegalArgumentException(ERR_UNKNOWN_URL + url);
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String defaultSort = null;
        qb.setTables(getTableName(code));
        if (isListCode(code)) {
            qb.setProjectionMap(getProjectionMap(code));
            defaultSort = getDefaultSortOrder(code);
            appendWhere(qb, code, url);
        } else {
            qb.appendWhere(Data._ID + "=" + url.getPathSegments().get(1));
        }
        SQLiteDatabase db = helper.getReadableDatabase();
        String orderBy = (TextUtils.isEmpty(sort)) ? defaultSort : sort;
        Cursor c = qb.query(db, projection, selection, selectionArgs, null,
            null, orderBy);
        c.setNotificationUri(getContext().getContentResolver(), url);
        return c;
    }
    
    ...

See: