アプリケーション内のデータを公開したい場合に、Content Providerを作成あるいは呼び出すことが出来ます。Content Providerとは、全てのアプリケーションからデータの読み書きが可能なオブジェクトで、パッケージ間でデータ共有を行う唯一の手段です(訳注:許可情報の設定を利用する方法もあるので、必ずしも唯一とは言えない。推奨される唯一の手段ではあれど)。Androidには音声、映像、画像、連絡先情報といった一般的なデータタイプ用のContent Providerが多く含まれています。AndroidのネイティブなContent Providerの一例がproviderパッケージ内にありますのでご参照ください。
Content Providerがデータをどのように処理するか、という点についてはそれぞれの実装に依存しますが、全てのContent Providerが従うべきデータ要求の受け入れ方と結果の返し方についての規約がります。しかしながら、Content Provider側ではその公開する情報に応じた、データ保存・取得を単純化する独自のヘルパ関数を実装することも出来ます。
本稿はContent Providerに関する以下の2つのトピックについて書かれています。
このセクションでは自身で実装した、あるいは他の開発者により実装されたContent Providerをどのように利用してデータの読み書きを行うかについて解説します。Androidでは音楽や画像ファイルから電話番号に至る広い範囲のデータタイプのためのContent Providerを提供しています[※1] 。android.providerパッケージ内の便利なクラス経由で公開されているContent Providerの一覧もご覧下さい。
AndroidのContent Providerはクライアントとゆるやかに結合しています。各Content Providerは、自身が処理するデータタイプを特定するためのユニークな文字列(URI)を公開し、クライアントは当該タイプのデータを読み書きするために必ずそのユニークな文字列を用いるという具合です。これについては今のところ概要に留めておき、のちほどQuerying for Dataで詳細を述べます。
本セクションでは以下の流れ[※2] を説明します。
各Content ProviderはContentURIでラッピングされるユニークな公開URIを持ち、それを用いてクライアントがContent Provider上でのデータ取得・追加・更新・削除を行います[※3] 。このURIを表記する方法は2つあります。1つは当該タイプのデータ全てを指す(例えば全ての連絡先)もので、もう1つは当該タイプのデータのうち、特定のレコードを指す(前の例にあわせるとジョー・スミスの連絡先)ものです。
アプリケーションはデバイスに対して、アイテムの一般的なタイプを指定してデータ取得を行う(前述の例では全電話番号)クエリか、あるいは特定のアイテムを取得する(同、ボブの電話番号)クエリのいずれかを発行することが出来ます。するとAndroidは特定カラムセットを持つ結果レコードセットのカーソルを返します[※4] 。以下に、クエリ文字列と結果セット(分かり易くするために一部省略しています)のサンプル[※5]を示します。
クエリ = content://contacts/people/
結果:
| _ID | _COUNT | NUMBER | NUMBER_KEY | LABEL | NAME | TYPE |
|---|---|---|---|---|---|---|
| 13 | 4 | (425) 555 6677 | 425 555 6677 | California office | Bully Pulpit | Work |
| 44 | 4 | (212) 555-1234 | 212 555 1234 | NY apartment | Alan Vain | Home |
| 45 | 4 | (212) 555-6657 | 212 555 6657 | Downtown office | Alan Vain | Work |
| 53 | 4 | 201.555.4433 | 201 555 4433 | Love Nest | Rex Cars | Home |
ご覧のように、クエリ文字列は通常のSQLクエリ文字列ではなく欲しいデータを表すURI文字列です。このURLは3つの部分から成っています。”content://”文字列、その後に続くどのような種類のデータが欲しいかという情報、そして指定コンテンツタイプの中で取得したいアイテムのID(任意指定)です。[※6] もう少しクエリ文字列の例を示しておきます。
通常のフォーマットがありながらも、クエリURIはなんだか複雑です。そこで、Androidではandroid.providerパッケージ内でこれらのクエリ文字列作成を肩代わりし、開発者がそれぞれのURIを知らなくても利用出来るようにしてくれるヘルパクラス群を提供しています。これらのクラスは各データタイプ用の(実際にはContentURIと呼ばれるラッパクラスを用いて定義される)CONTENT_URIという文字列を定義しています。例えば、android.provider.contacts.People.CONTENT_URIはAndroidの組み込みpeople[※7] Content Providerから人を検索するクエリ文字列を定義しています。
通常はCONTENT_URIオブジェクトを利用してクエリ発行を行います。例外はこの文字列の末尾にID値を付けて特定レコードのデータを取得しようとする場合のみです。つまり、連絡先情報から23番レコードを探し出したい場合には以下のようなクエリを発行することになるでしょう。
// 連絡先のベースURIを取得する ContentURI myPerson = new ContentURI(android.provider.Contacts.People.CONTENT_URI.toURI()); // 必要なレコードIDを追加する(このIDは予め知っておく必要がある) myPerson.addId(23); // レコードを要求する Cursor cur = managedQuery(myPerson, null, null, null);
このクエリはデータベース結果セットのカーソルを返します。どのカラムが返されるのか、それらは何と呼ばれるのか[※8] 、何と名付けられるのかについては以下で説明しますので、今のところは特定のカラムのみを返すような指定、ソート順指定、SQLのWHERE句指定が可能であるとだけ押さえておいてください。
マネージドカーソルを取得するにはActivity.managedQuery()を使う必要があります。マネージドカーソルはアプリケーションの一時停止時に自分自身をアンロードした上でアプリケーションの再開時に再度クエリを発行するといった細かな処理を行ってくれます[※9] 。Activity.startManagingCursor()を呼ぶことで、Androidにアンマネージドカーソルの管理を頼むことが出来ます。
では、連絡先名、電話番号、写真のそれぞれ一覧を取得するサンプルクエリを見てみましょう。
// 必要なカラムを指定する配列
// プロバイダは特定クエリに対して返すカラム名の一覧を公開しますが、
// 全カラムの一覧を取得した上でイテレート処理をかけることも出来ます。
string[] projection = new string[] {
android.provider.BaseColumns._ID,
android.provider.Contacts.PeopleColumns.NAME,
android.provider.Contacts.PhonesColumns.NUMBER,
android.provider.Contacts.PeopleColumns.PHOTO
};
// クエリ取得の最良手段。マネージドクエリを返します。
Cursor managedCursor = managedQuery( android.provider.Contacts.Phones.CONTENT_URI,
projection, // 欲しいカラム一覧
null, // WHERE句。ここでは未指定
android.provider.Contacts.PeopleColumns.NAME + "ASC"); // ORDER BY句
このクエリでは電話番号連絡先プロバイダ[※10] に保存されている全ての電話番号取得を行います。各連絡先情報には名前とユニークなレコードIDが含まれますので、次のセクションで解説するように結果セット中を先に進んだり前に戻ったり、レコードの追加や削除、編集といったことが出来るようになります。
クエリは0個以上のデータベースレコードセットを返します。カラム名、並び順、タイプはContent Providerごとに特有のものですが、全てのクエリは当該行にあるアイテムのIDを示す、_idというカラムを持ちます。クエリがビットマップや音声ファイルといったバイナリデータを返そうとする場合、データ取得対象のcontent://で始まるURIが書き込まれたカラムを持つことになるでしょう。詳細は後述とし、ここではさきのクエリによる結果の例示のみを行います。
| _id | name | number | photo |
|---|---|---|---|
| 44 | Alan Vain | 212 555 1234 | content://images/media/123 |
| 13 | Bully Pulpit | 425 555 6677 | content://images/media/128 |
| 53 | Rex Cars | 201 555 4433 | content://images/media/332 |
この結果セットはカラムの一部のみを指定した際に返されるものを表しています。任意項目のカラムリストをprojection パラメータに指定した場合にこのような結果となります。Content Provider側は、各カラムの説明となるインターフェイスセットを実装する方法(例えばContacts.People.PhonesではBaseColumns、PhonesColumns、PeopleColumnsを拡張しています)か、あるいはカラム名群を定数としてリストする方法かのいずれかの方法により、どのようなカラムが指定可能であるかを公開する必要があります。Content Providerにより公開されているカラムのデータタイプが分からなければ読み込みが行えず(フィールド読み込みメソッド自体がデータタイプごとに異なるため)、またカラムのデータタイプ情報はプログラム的に公開されないことに注意が必要です[※11]。
得られたデータは結果セット内を進んだり戻ったりとイテレート出来るCursorオブジェクトにより[※12] 公開されます。このカーソルを利用して行の読み込み、編集、削除を行うことが出来ます。なお行追加については、後述する別のオブジェクトが必要です。
規約により全ての結果セットは特定レコードのIDを示す_idフィールドと現在の結果セット内にある行数を意味する_countを持つ必要があることに注意して下さい。これらのフィールド名はBaseColumnsにおいて定義されています。
前述のクエリではデータセット内でどのようにファイルが返されるかを示しました。ファイルフィールドは通常(必須ではありませんが)ファイルの文字列パスです。しかし呼び出し側でそのファイルに直接アクセスを試みても許可情報の問題などによりアクセス出来ません。代わりにContentResolver.openInputStream()やContentResolver.openOutputStream()といったContent Providerのヘルパ関数を利用することになります。
クエリにより取得されたカーソルオブジェクトを使い、結果のレコードセットにアクセスすることが出来ます。ID指定により単一レコードを要求した場合このセットには1つの値しか入ってきませんが、そうでない場合は複数の値を含むことが考えられます。レコード内の特定フィールドからデータを読み込むことも出来ますが、各データタイプごとに特有の読み込みメソッドが存在するために予めフィールドのデータタイプを知っておく必要があります(ほとんどのカラムタイプに対し、文字列として読み取るメソッドを利用するとAndroidは当該データの文字列表現を返します)。カーソルを利用すると、カラム名をインデックス番号から取得したり、逆にカラム名からインデックス番号を取得したりすることが出来ます。
画像ファイルなどのバイナリデータを読み込んでいる場合は、絡むに格納されているcontent://のURIについてContentResolver.openOutputStream()を呼ぶ必要があります。
以下のコード断片は電話番号クエリから名前と電話番号を読み出す例を示しています。
private void getColumnData(Cursor cur){
if(!cur.first())
return;
String name;
String phoneNumber;
int nameColumn = cur.getColumnIndex(android.provider.Contacts.PeopleColumns.NAME);
int phoneColumn = cur.getColumnIndex(android.provider.Contacts.PhonesColumns.NUMBER);
int pathColumn = cur.getColumnIndex(android.provider.Contacts.PeopleColumns.PHOTO);
String imagePath;
while (cur.next()) {
// フィールド値を取得する
name = cur.getString(nameColumn);
phoneNumber = cur.getString(phoneColumn);
imagePath = cur.getString(stringColumn);
InputStream is = getContentResolver().openInputStream(imagePath);
... ファイルストリームを何かに読み込む...
is.close()
// 値に対する処理を行う
...
}
}
繰り返しになりますが、ここでの画像フィールドにはファイルのパスが入っています。Content Providerによっては、ファイルなどのフィールド内容をより簡単に取得するためのヘルパメソッドを提供しているものもあります。
なお、カーソルクラスに対して更新メソッドを呼び出す際には、commitUpdates()を呼ばなければデータベースへの変更反映が為されないことに注意して下さい。
各レコードを更新するには、カーソルを正しいオブジェクトに割り当て、正しいupdate…系メソッドを呼び出し、そしてcommitUpdates()を呼び出す必要があります。
複数レコードを一気に更新する場合(例えば、連絡先フィールドにある全ての”NY”を”New York”に変更したい場合)には変更したいカラムと値を付けてContentResolver.update()を呼ぶことになります。
繰り返しますが、カーソルクラス上で更新メソッドを呼び出した後でcommitUpdates()を呼び出して変更をデータベースに反映するのを忘れないようにしてください。
レコードを追加するには、追加したいアイテムのタイプを示すURIと新レコードを初期化するためのMapを引数に、ContentResolver.insert()を呼び出します。このメソッドは追加されたレコードの番号を含む完全なURIを返すので、それを用いてクエリを発行し、新レコードのカーソルを取得することが出来ます。
例によって、カーソルクラスの更新系メソッドを呼び出した後でcommitUpdates()を呼び出して変更をデータベースに反映するのを忘れないでください。
ファイルを保存するには、以下に示すコード断片にあるようなURIを付けてContentResolver().openOutputStream()を呼び出す方法と、もう一つ後のサンプルにあるようにandroid.provider.Media.Images.insertImage()を使う方法の2つがあります。
// ファイル名と説明文をmapに保存する。キーはContent Providerの
// カラム名で、値は当該レコードのフィールドに保存したい値を指定する。
HashMap<String, Object> values = new HashMap<String, Object>();
values.put(Media.Images.NAME, "road_trip_1");
values.put(Media.Images.DESCRIPTION, "Day 1, trip to Los Angeles");
// ビットマップは除いて(しかし値付きの)新規レコードを投入すると、
// 新レコードのURIを返す。
ContentURI uri = getContentResolver().insert(Media.Images.CONTENT_URI, values);
// 新しいレコードのファイルハンドラを取得し、データを書き込む。
// sourceBitmapはデータベースに保存したいファイルのBitmapオブジェクト。
OutputStream outStream = getContentResolver.openOutputStream(uri);
sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
Or, using a convenience class:
android.provider.Media.Images.insertImage( getContentResolver(),
sourceBitmap,
"road_trip_1",
"Day 1, trip to Los Angeles");
単一レコードを削除するには、削除対象の行URIを付けてContentResolver.delete()を呼ぶか、Cursor.deleteRow()を呼ぶかのいずれかを行います。
複数行を削除したい場合は、削除したいレコードのタイプを表すURI(例えば、android.provider.Contacts.People.CONTENT_URI)と削除対象を特定するためのSQLにおけるWHERE句を付けてContentResolver.delete()を呼び出します。当然ながら、書き込むWHERE句が間違っていれば必要以上に行削除を行ってしまう可能性もあるので十分注意して下さい。
例によって例のごとく、更新系メソッドを呼んだ後はcommitUpdates()を呼び、変更をデータベースに反映するのを忘れないようにしてください。
新たなタイプのデータを他のアプリケーションから読み書きするための、独自Content Providerを作成する方法を以下に示します。
独自のContent Provider例については、SDKに含まれているメモ帳内のNotePadProviderを参照して下さい。
最後に、コンテンツURIの重要な部分について再度まとめておきます。