Using ContentProviders to access the database

From F-Droid
Jump to: navigation, search

Note: All links to the F-Droid source code will point to an old version. This is to ensure that if anything changes, the article still makes sense and points to the correct lines of source.

Why Content Providers?

Content providers are a mechanism for accessing an sqlite database in Android. The reason you would use content providers over, say, our own class which connects to and queries the database, is because it integrates better with other parts of the Android eco system. Some of the places where using content providers makes coding an android app easier are:

  • Lazy loading of large lists when displaying ListViews
  • Testing infrastructure

Getting data from F-Droid's content providers

This section will discuss how you go about querying and using the content providers in F-Droid.

F-Droid consists of a few content providers, each is generally responsible for one database table:

There are two ways to get data out of a content provider in F-Droid:

  • Using a ContentResolver directly (if need to pass the data to an Android object such as a ListVie)
  • Using the Helper class for that provider (if you want to work with the result set yourself)

Accessing providers using a content resolver

NOTE: Most of the time, you will not want to do this, but rather make use of the #Helper classes. This is presented first though, so that you can get a better idea of why Helper classes exist and the problem they solve

Content resolvers are available from the getContentResolver() method of a Context. Here is an example of accessing data from an F-Droid content provider using a content resolver:

Cursor resultCursor = context.getContentResolver().query(
  AppProvider.getInstalledUri(), // Uri's are used to tell the provider what data is being requested, in this case "All installed apps"
  projection, // see section on Data Columns / Projection below...
  null, null, null // We don't make use of these parameters in the content provider
);

The cursor is a pointer to the result set. The number of results is available in resultCursor.getCount(), and you can iterate it with the resultCursor.moveToFirst(), resultCursor.isAfterLast(), resultCursor.moveToNext() set of methods. Once you have asked the cursor to moveTo a location in the result set, it will be pointing to a specific installed app. To get back details of that app, you can say:

String id = resultCursor.getString( resultCursor.getColumnIndex( AppProvider.DataColumns.APP_ID ) );

However that is a little hairy, and a better way is to construct a value object:

App app = new App( resultCursor );
String id = app.id;

Helper classes

Each F-Droid data provider has an associated Helper inner classss, which provide helper methods for querying the content provider. The reasoning for this is that generally, content providers only return a Cursor, which is not a very safe way to access data. The Helper class will have a set of methods that ensure you are returned instances of the relevant value object. For example, the AppProvider.Helper class returns instances of the App value object class.

public class AppProvider extends FDroidProvider {
  public staitc class Helper {
    ...
  }
}

If we wanted to query the AppProvider for all apps, then a helper class changes this code:

Cursor cursor = getContentResolver().query( AppProvider.getContentUri(), projection, null, null, null );
if ( cursor != null ) {
  List<App> allApps = new ArrayList<App>( cursor.getCount() );
  cursor.moveToFirst();
  while ( !cursor.isAfterLast() ) {
    allApps.add( new App( cursor ) );
    cursor.moveToNext();
  }
  cursor.close();
}

into the following:

List<App> allApps = AppProvider.Helper.all( getContentResolver(), projection );

If a Helper class doesn't have the method that you need, feel free to add it. For example, if you want the AppProvider.Helper class provide an easy way to access all of the installed apps, then you may want to create a findInstalled static method in the AppProvider.Helper class.

Specifying which columns to return

When asking a content provider for information, you will also have to specify which field you are interested in it returning. For example, the AppProvider manages a table in the database which has quite a few fields, some of which are description fields with a lot of text. If you are querying the list for "all apps" for the main list, then you should not ask for each apps "description" field. This will not end up being displayed, and the time it takes the database to fetch this and put it in a String so that Java can use it will add up.

Each F-Droid content provider has an inner interface called DataColumns, which specifies the columns you are allowed to ask for when interacting with a content provider.

The following code will ensure that only two fields will be made availale after querying the database (I've left the comma after SUMMARY on purpose, as it is valid Java syntax, and makes adding new lines by copying and pasting the last one, then modifying it, easier).

String[] projection = {
 AppProvider.DataColumns.APP_ID,
 AppProvider.DataColumns.SUMMARY,
};
List<App> app = AppProvider.Helper.all( getContentResolver(), projection );

WARNING: When accessing the App objects which are returned from the query above, the only fields which will be set are app.id and app.summary. The Java compiler will allow you to try and access, e.g. app.description, however it will always be null.

Joining to other providers and returning their data

Some providers will allow you to ask for data that is actually stored in another provider. For example, the AppProvider has the ability to join onto the table managed by the ApkProvider, and fetch the name of the suggested app version. In this case, the version code of the suggested version is stored in the app table, whereas that versions name is stored in the apk table (and usually managed by the ApkProvider).

To get the suggested version name when querying the AppProvider, you can do use the following projection:

String[] projection = {
  AppProvider>DataColumns.APP_ID,
  AppProvider.DataColumns.SUGGESTED_VERSION_CODE,
  AppProvider.DataColumns.SuggestedApk.VERSION,
};

Responding to changes in the database

Quite often, we want to be able to be notified when the database changes, even though we were not the person who caused the change to occur. Usually, this is in order to update the user interface to better reflect the data we are presenting. For example, if the list of apps shows the version number of each app, but then the app is updated , the version number in the UI should also be updated.

This pattern is common in Android, and in fact many widgets have built in support for responding to data changes. The most obvious example is a list of items, which represents a bunch of rows in the database. In the case of F-Droid, this could be the list of apps on the main screen, which represents the rows from the AppProvider. The main list of apps belongs to a Fragment which implements LoaderCallbacks<Cursor>. What this interface does is get notified when the underlying data changes, and then it re-queries the content provider to get an up-to-date list of apps. When this up-to-date list is returned from the LoaderCallback, the ListView automatically re-populates the UI with the new items.

We can perform this sort of "responding to database changes" ourself too, by passing our own ContentObserver to the ContentResolver.registerContentObserver method..

getContentResolver().registerContentObserver(
  AppProvider.getAppUri( app ),
  true,
  new ContentObserver() {

    @Override
    public void onChange(boolean selfChange, Uri uri) {
      // Do something, e.g. update the UI in response to the change
    }

  }
}

ContentObserver considerations

The main thing to consider when registering content observers, is what URI you need to listen to. In the example above, we are listening for changes to the AppProvider.getAppUri( app ) URI. However, that means that if a change occurs which affects every single element, then we will not get notified. Indeed, in the example above, we probably want to listen for the more general "AppProvider.getContentUri()" URI, which responds to changes to any app. The reason for this is that when doing a repo update, we wait until the end, when all apps have been processed, and send one change notification for AppProvider.getContentUri(). Although it would be possible to send an individual AppProvider.getAppUri( app ) notification for each app that is updated, this would cause the main list of apps to get refreshed 1,000 times during a repo update.

TODO: Add more/better examples to teh ContentObserver section.