Creating content providers

If an app wants to allow its data to be available to other apps on the devices, it needs a way to control access. Content providers can be used as a public endpoint to the data, but they allow the app to maintain control over the data.

How to do it...

Creating content providers allows us to share data with other apps in a uniform manner. It is, for the most part, straightforward. Let's take a look at the following steps:

  1. First, we inherit from the ContentProvider base type:
    public class NumberStringsContentProvider : ContentProvider {
      public override bool OnCreate () {
      }
      public override string GetType (Uri uri) {
      }
      public override ICursor Query (
        Uri uri, string[] projection,
        string selection, string[] selectionArgs,
        string sortOrder) {
      }
      public override int Delete (
        Uri uri, string selection, string[] selectionArgs) {
      }
      public override Uri Insert (
        Uri uri, ContentValues values) {
      }
      public override int Update (
        Uri uri, ContentValues values,
        string selection, string[] selectionArgs) {
      }
    }
  2. Then, we set up the content URI and authority that is used to access our provider:
    public const string Authority = "com.xamarincookbook.provider";
    private const string BasePath = "numberstrings";
    public static readonly Uri ContentUri = Uri.Parse("content://" + Authority + "/" + BasePath);
  3. Using that authority, we add an attribute to this type:
    [ContentProvider(
      new [] { NumberStringsContentProvider.Authority })]
  4. Given the authority, we can create the various URI patterns or data paths:
    private enum Code {
      All,
      ByNumber,
      ByText
    };
    private static UriMatcher uriMatcher;
    static NumberStringsContentProvider() {
      uriMatcher = new UriMatcher(UriMatcher.NoMatch);
      uriMatcher.AddURI(Authority, BasePath, (int)Code.All);
      uriMatcher.AddURI(Authority,
        string.Format("{0}/#", BasePath), (int)Code.ByNumber);
      uriMatcher.AddURI(Authority,
        string.Format("{0}/*", BasePath), (int)Code.ByText);
    }
  5. For each of the URI patterns, we define a MIME type:
    public static class MimeTypesConsts {
      public static readonly string NumberStrings = string.Format("{0}/vnd.{1}.NumberStrings",
        ContentResolver.CursorDirBaseType, Authority);
      public static readonly string String = string.Format("{0}/vnd.{1}.NumberString",
        ContentResolver.CursorItemBaseType, Authority);
      public static readonly string Number = string.Format("{0}/vnd.{1}.NumberNumber",
        ContentResolver.CursorItemBaseType, Authority);
    }
  6. Now that we have our definitions in place, we can start the implementation. The first thing to do is to return the correct MIME type for a particular URI pattern. This is done by implementing the GetType() method:
    public override string GetType(Uri uri) {
      switch ((Code)uriMatcher.Match(uri)) {
        case Code.All:
          return MimeTypesConsts.NumberStrings;
        case Code.ByNumber:
          return MimeTypesConsts.Number;
        case Code.ByText:
          return MimeTypesConsts.String;
        default:
          throw new Java.Lang.IllegalArgumentException();
      }
    }
  7. As this provider is not going to do much, we can create an empty implementation for the OnCreate() method:
    public override bool OnCreate() {
      return true;
    }
  8. In this example, we will make a simple two-column table with one column for an integer and another column for the string representation of that number:
    private Tuple<int, string>[] data = {
      new Tuple<int, string>(0, "Zero"),
      new Tuple<int, string>(1, "One"),
      new Tuple<int, string>(2, "Two"),
      ...
    };
  9. We also need to create a set of columns for this provider to be used in projections:
    public static class InterfaceConsts {
      public const string Number = "number";
      public const string Text = "text";
    
      public static string[] AllColumns = {
        InterfaceConsts.Number,
        InterfaceConsts.Text
      };
    }
  10. Using this, we can implement the Query type to return data based on the parameters (in this example, we just assume the projection is AllColumns):
    public override ICursor Query(
      Uri uri, string[] projection,
      string selection, string[] selectionArgs,
      string sortOrder) {
        Code code = (Code)uriMatcher.Match(uri);
        if (code == Code.All) {
          MatrixCursor cursor = new MatrixCursor(projection);
          foreach (var item in data) {
            var builder = cursor.NewRow();
            // create the row based on the projection
            builder.Add(item.Item1).Add(item.Item2);
          }
          return cursor;
        }
        // handle the rest of the parameters
      }
  11. Similarly, an implementation can be created for the Delete(), Insert(), and Update() methods. We can implement them according to how we want the provider to work with the requests. We read the parameters and decide on what should be done.

How it works...

A content provider makes data available to other apps, which is why we create a content provider if our app needs to share data with other apps. However, we can make content providers even if we only wish to make the data available within our app. But, this does reduce the amount of code that can be shared across other platforms. In order to increase code sharing, our provider can just be a wrapper to the shared data.

Data is shared using a structure similar to a database table with the queries and commands similar to that of SQL. Although requests aren't entirely text-based, the where clause is partially string-based. This allows for a semi type-safe interface. The data requested is returned as a cursor to the first row in the result and moved sequentially through the result set.

Content providers are accessed through a URI, which contains two main parts, the authority which identifies the provider, and the path, which points to a specific resource in the provider. The authority is usually similar to that of the app package name, being a unique, reverse Internet domain name. The path is usually the table name or resource.

For example, if we have an app that has the app package name of com.cookbook.fungame, and we have a provider that accesses some form of scoreboard, we can use the com.cookbook.fungame.provider authority and the scores path. We can also include an ID to a particular user, 22. In this case, our content URI will be content://com.cookbook.fungame.provider/scores/22.

When responding to requests, we use a UriMatcher() method to distinguish the content URI that was passed to the provider. We construct the matcher from a URI set consisting of the authority, path, and, optionally for some entries, wildcards. Wildcards can be the asterisk, *, for any string or the hash, #, for any series of digits.

The data that is shared can exist in any type or form. This is one of the reasons for using a content provider: creating a uniform interface for any data type. Data can be from a database, XML, or even from a remote, network-based data store.

If we will be providing data in a tabular form, we should have a primary key or ID column to identify a particular row or item. This column can have any name; however, the best choice is to use the value of BaseColumns.ID. Using this name is required when linking a result set to a list view, as one column is required to have the name _id. For these operations, we implement the Query(), Insert(), Update(), and Delete() methods.

If we will be providing data in the form of files, we implement this by overriding the OpenFile() method for arbitrary files or OpenAssetFile for assets. In these methods, we open the file and return it to the consumer.

When creating our content provider, we inherit from the ContentProvider type and register it with the Android system by applying the [ContentProvider] attribute, using the authority URI as a parameter.

The provider is not automatically launched when the app is launched. When the provider is first accessed by a consumer, only then is the OnCreate() method called.

Tip

Code executed in the OnCreate() method should do as little as possible, and tasks should be deferred until they are actually needed.

The Query(), Insert(), Update(), and Delete() methods are each a varying combination of parameters, including a projection, filter clause, value collections, and a sort parameter. We use these parameters to build up and execute queries, which return the data or some other result to the consumer. These methods need to be thread-safe as we can call them multiple times on different threads.

The Query() method should always return an ICursor type. If an error occurs, throw an exception, and if no data is available, return an empty cursor. The Insert() method should add a new item to the data source and return the URI to that item. The Delete() and Update() methods should return the number of items removed or updated, if any.

The GetType() method returns a string representing the MIME type of the data returned, using Android's vendor-specific MIME format. There are two parts, the Android-specific part and the provider-specific part, separated by a slash.

There are two options available for the Android-specific part. For multiple rows, we use the vnd.android.cursor.dir or ContentResolver.CursorDirBaseType fields, and the vnd.android.cursor.item or ContentResolver.CursorItemBaseType fields for single row results. The next part is a combination of the provider authority and the specific type. Continuing with the preceding example, we will use vnd.android.cursor.dir/vnd.com.cookbook.fungame.provider.scores for multiple rows and vnd.android.cursor.item/vnd.com.cookbook.fungame.provider.scores for a single row.

See also

  • The Consuming content providers recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.32.86