コンテンツプロバイダーチュートリアル

コンテンツプロバイダーとは何か?

Androidのアプリケーションを起動するユーザーはアプリケーションで固有であり、ファイルアクセスも排他的になるため、アプリケーション間で情報を交換する場合はintentの中に情報を詰めるか、AIDLを使ったリモートメソッド呼び出しをする方法がある。しかし、それらの方式だとアプリケーション間の結合が強すぎて柔軟性に欠ける。それらの欠点を補うアプリケーション間の情報共有方式がコンテンツプロバイダーとなる。

コンテンツプロバイダーのデータの扱い

コンテンツプロバイダーはURIを使用してデータのエントリーを表現している。例えば、アドレス帳のコンテンツプロバイダーはcontent://contacts/people/1で一番目のアドレス情報、content://contacts/people/2で二番目のアドレス情報、content://contacts/people/で全体のアドレス情報のエントリを表現している。(つまり、ディレクトリの様な階層型データベースとなっている。)

Android標準のコンテンツプロバイダーはcontent://xxxxx/~という形式になっているが、勝手に追加する場合はcontent://com.suddenAngerSystem.xxxx/service/111の用に"content://"+パッケージ名+"."+クラス名+"/"+データカテゴリ+"/"+データのidといった形式で重複することの無いように定義する必要がある。

コンテンツプロバイダーを利用する

具体例としてアドレス帳から最初のエントリを取得してみよう。アドレス帳にアクセスするためにはセキュリティ権限が必要なため、まずはマニフェストファイルにアドレス帳への読み込みアクセス権、書き込みアクセス権を設定する。

<?xml version="1.0" encoding="utf-8"?<
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.suddenAngerSystem"
      android:versionCode="1"
      android:versionName="1.0"<
    <application android:icon="@drawable/icon" android:label="@string/app_name"<
        <activity android:name=".ContentProviderUser"
                  android:label="@string/app_name"<
            <intent-filter<
                <action android:name="android.intent.action.MAIN" /<
                <category android:name="android.intent.category.LAUNCHER" /<
            </intent-filter<
        </activity<
    </application<
    <uses-sdk android:minSdkVersion="3" /<

<uses-permission android:name="android.permission.READ_CONTACTS"<</uses-permission<
<uses-permission android:name="android.permission.WRITE_CONTACTS"<</uses-permission<
</manifest<
	

次に実際の取得処理を見てみる。ContentUrisサービスクラスを使用してアクセスのためのURIを作成する。(android.provider.Contacts.People.CONTENT_URIはcontent://contacts/people/の変わり。)次にmanagedQueryメソッドでデータ取得のためのカーソルクラスのインスタンス(DB操作でありがちなアクセス方式)を取得する。managedQueryは引数で射影操作、選択操作、選択操作の引数、ソート順を指定できるが、ここでは簡単のために指定しない

package com.suddenAngerSystem;

import android.app.Activity;
import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;

public class ContentProviderUser extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //最初のレコードを取得
        final Uri uri = ContentUris.withAppendedId(android.provider.Contacts.People.CONTENT_URI, 1);
        final Cursor result = managedQuery(uri, null, null, null, null);
        //名前を格納した列のインデックスを取得
        final int index = result.getColumnIndex(android.provider.Contacts.People.NAME);
        //最初の要素を取得(本当は成功したかどうかチェックすべきだが例ためしない)
        result.moveToNext();
        final String name1 = result.getString(index);
    }
}
	

上記の様な単純なクエリだけでなく、コンテンツプロバイダーはDBの様に使用することができる。DBアクセスの典型例CRUD(Create Read Update Delete)を実例で以下に示す。ポイントはRead以外の場合はgetContentResolverで取得したContentResolverのメソッドを使用しているということになる。(実はContentResolverからもクエリは発行できる。)また、ContentResolverのリファレンスを読んで貰えば分かるが、updateとdeleteはほとんどSQLそのままのアクセス方式となっている。

package com.suddenAngerSystem;

import android.app.Activity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Contacts.People;

public class ContentProviderUser extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //名前がfoo barの人をアドレス帳に追記。
        ContentValues values = new ContentValues();
        values.put(android.provider.Contacts.People.NAME, "foo bar");
        values.put(android.provider.Contacts.People.STARRED, true);
        final Uri uri = getContentResolver().insert(android.provider.Contacts.People.CONTENT_URI, values);

        final Cursor result = managedQuery(android.provider.Contacts.People.CONTENT_URI, null, null, null, null);
        result.moveToNext();
        final String name = result.getString(result.getColumnIndex(android.provider.Contacts.People.NAME));

		//アドレス帳の名前がfoo barの人のeメールアドレスを追記。
		values.clear();
		values.put(android.provider.Contacts.People.ContactMethods.KIND, android.provider.Contacts.KIND_EMAIL);
		values.put(android.provider.Contacts.People.ContactMethods.DATA, "foo@xxx.jp");
		values.put(android.provider.Contacts.People.ContactMethods.TYPE, android.provider.Contacts.People.ContactMethods.TYPE_HOME);
		getContentResolver().insert(Uri.withAppendedPath(uri ,android.provider.Contacts.People.ContactMethods.CONTENT_DIRECTORY), values);

		//アドレス帳の名前がfoo barの人の名前を変更。
		values.clear();
		values.put(android.provider.Contacts.People.NAME, "foo bar2");
		getContentResolver().update(uri, values, null, null);

		//名前がfoo bar改め、foo bar2の人をアドレス帳から削除。
        getContentResolver().delete(uri, null, null);

    }
}
	

コンテンツプロバイダーを実装する

コンテンツプロバイダーを利用するで見た通り、コンテンツプロバイダーは実装クラスをユーザー側は指定していない。ということはどこかで実装クラスを判断しているはずである。結論から言うと、アプリケーションはマニフェストファイルでコンテンツプロバイダーを宣言することによってシステムにそのコンテンツプロバイダーが登録される。そして、ユーザー側から要求があった場合はシステムで適切なコンテンツプロバイダーを選択して、ユーザーに対して実装クラスを渡している。また、コンテンツプロバイダーは先ほど見たようなCRUDの実装をしなければならない。具体的にはContentProviderクラスを継承して必要なメソッドをオーバーライドすることとなる。

具体例として乱数表のコンテンツプロバイダーを作成してその実装を見てみる。まず、マニフェストファイルの記載は以下の様になる

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.suddenAngerSystem"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">

    <provider android:name="ContentProviderProvider" android:authorities="com.suddenAngerSystem.randomNumber"></provider>
</application>
    <uses-sdk android:minSdkVersion="3" />
</manifest> 
	

次にSQLアクセスのためのサポートクラスを作成する。これは作らなくても良いのだが、SQLiteのテーブル作成などに便利である。具体的にはこのクラスからdbへのアクセスインターフェースを取るとテーブルができていない場合はテーブルを作ってくれて、テーブルのバージョンが上がるとその差分を修正してくれる。

//サポートクラス
class SQLiteSupportImpl extends SQLiteOpenHelper {
	private static final String DATABASE_NAME = "randomNumber.db";
	private static final int DATABASE_VERSION = 1;
	public SQLiteSupportImpl(Context ctx) {
		super(ctx, DATABASE_NAME, null, DATABASE_VERSION);
	}

	//作成時の動作
	@Override
	public void onCreate(SQLiteDatabase db) {
		//直接埋め込みは本当は汚い。
		db.execSQL(
			"CREATE TABLE random ("
			+ android.provider.BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
			+ "randomNumber INTEGER);");
		//100個乱数を生成
		for(int i = 0; i < 100; ++i) {
			Random randGen = new Random();
			final int randomNumber = randGen.nextInt();
			db.execSQL("INSERT INTO random (randomNumber) VALUES (" + Integer.toString(randomNumber) + ");");
		}
	}

	//DBバージョンが上がった場合の動作
	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		//削除してまた作成
	    db.execSQL("DROP TABLE IF EXISTS random");
	    onCreate(db);
	}
}
	

今度はコンテンツプロバイダーの実装を見てみよう。カスタムコンテンツプロバイダーはContentProviderクラスを継承し、それのメソッドをオーバーライドする必要がある。乱数表のコンテンツプロバイダーでは実体のデータをSQLiteのテーブルに格納しているため、データアクセスは要求をそのままSQLiteにSQLとして発行するだけである。そのため、コンテンツプロバイダーではURIをパースして乱数表全体に対する操作か、それとも乱数表の一行に対する操作なのかを判断することと、後者だった場合は要求行を取り出してSQLにするだけである。

public class ContentProviderProvider extends ContentProvider {
	public static final String AUTHORITY = "com.suddenAngerSystem.randomNumber";
	public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + "numbers");
	private static final int CODE_NUMBERS = 1;
	private static final int CODE_NUMBER = 2;

   /** ディレクトリのMIMEタイプ */
   private static final String CONTENT_TYPE
      = "vnd.android.cursor.dir/com.suddenAngerSystem.randomNumber";

   /** 単一のMIMEタイプ */
   private static final String CONTENT_ITEM_TYPE
      = "vnd.android.cursor.item/com.suddenAngerSystem.randomNumber";

	private UriMatcher uriMatcher_;
	private SQLiteSupportImpl sqlSupport_;

	//コンテンツプロバイダーの生成時
	@Override
	public boolean onCreate() {
		//何にもマッチしないものを作成
		uriMatcher_ = new UriMatcher(UriMatcher.NO_MATCH);
		//マッチ対象にcom.suddenAngerSytem.randomNumber/numbersを追加
		uriMatcher_.addURI(AUTHORITY, "numbers", CODE_NUMBERS);
		//マッチ対象にcom.suddenAngerSytem.randomNumber/numbers/xxxxを追加(xxxxは数字)
		uriMatcher_.addURI(AUTHORITY, "numbers/#", CODE_NUMBER);
		//DBを初期化(必要であれば、勝手にテーブルを作ってくれる)
		sqlSupport_ = new SQLiteSupportImpl(getContext());
		return true;
	}

	//URIからリクエストされた種別に変換する
	@Override
	public String getType(Uri uri) {
		switch (uriMatcher_.match(uri)) {
		case CODE_NUMBERS:
			return CONTENT_TYPE;
		case CODE_NUMBER:
			return CONTENT_ITEM_TYPE;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}
	}


	//指定内容をdelete
	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		final SQLiteDatabase db = sqlSupport_.getWritableDatabase();
		final int deleteCount;
		switch (uriMatcher_.match(uri)) {
		case CODE_NUMBERS:
			deleteCount = db.delete("random", selection, selectionArgs);
			break;
		case CODE_NUMBER:
			final long id = Long.parseLong(uri.getPathSegments().get(1));
			final String idPlusSelection = android.provider.BaseColumns._ID + "=" + Long.toString(id) + (selection == null ? "" : "AND (" + selection + ")");
			deleteCount = db.delete("random", idPlusSelection, selectionArgs);
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		return deleteCount;
	}


	//指定内容を追加
	@Override
	public Uri insert(Uri uri, ContentValues values) {
		final SQLiteDatabase db = sqlSupport_.getWritableDatabase();
		if(uriMatcher_.match(uri) != CODE_NUMBERS) {
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		final long id = db.insertOrThrow("random", null, values);

		// 変更を通知する
	    final Uri newUri = ContentUris.withAppendedId(CONTENT_URI, id);
	    getContext().getContentResolver().notifyChange(newUri, null);

		return newUri;
	}

	//指定のクエリー
	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		if (uriMatcher_.match(uri) == CODE_NUMBER) {
			final long id = Long.parseLong(uri.getPathSegments().get(1));
			selection = android.provider.BaseColumns._ID + "=" + Long.toString(id) + (selection == null ? "" : "AND (" + selection + ")");
		}
		final SQLiteDatabase db = sqlSupport_.getWritableDatabase();
		Cursor cursor = db.query("random", projection, selection, selectionArgs, null, null, sortOrder);

		cursor.setNotificationUri(getContext().getContentResolver(), uri);

		return cursor;
	}

	//指定内容を更新する
	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		final SQLiteDatabase db = sqlSupport_.getWritableDatabase();
		final int updateCount;
		switch (uriMatcher_.match(uri)) {
		case CODE_NUMBERS:
			updateCount = db.update("random", values, selection, selectionArgs);
			break;
		case CODE_NUMBER:
			final long id = Long.parseLong(uri.getPathSegments().get(1));
			final String idPlusSelection = android.provider.BaseColumns._ID + "=" + Long.toString(id) + (selection == null ? "" : "AND (" + selection + ")");
			updateCount = db.update("random", values, idPlusSelection, selectionArgs);
			break;
		default:
			throw new IllegalArgumentException("Unknown URI " + uri);
		}

		// 変更を通知する
		getContext().getContentResolver().notifyChange(uri, null);

		return updateCount;
	}

}
	

参考資料