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.
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:
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) { } }
public const string Authority = "com.xamarincookbook.provider"; private const string BasePath = "numberstrings"; public static readonly Uri ContentUri = Uri.Parse("content://" + Authority + "/" + BasePath);
[ContentProvider( new [] { NumberStringsContentProvider.Authority })]
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); }
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); }
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(); } }
OnCreate()
method:public override bool OnCreate() { return true; }
private Tuple<int, string>[] data = { new Tuple<int, string>(0, "Zero"), new Tuple<int, string>(1, "One"), new Tuple<int, string>(2, "Two"), ... };
public static class InterfaceConsts { public const string Number = "number"; public const string Text = "text"; public static string[] AllColumns = { InterfaceConsts.Number, InterfaceConsts.Text }; }
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 }
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.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.
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.