Content Providerの利用

アプリケーション内のデータを公開したい場合に、Content Providerを作成あるいは呼び出すことが出来ます。Content Providerとは、全てのアプリケーションからデータの読み書きが可能なオブジェクトで、パッケージ間でデータ共有を行う唯一の手段です(訳注:許可情報の設定を利用する方法もあるので、必ずしも唯一とは言えない。推奨される唯一の手段ではあれど)。Androidには音声、映像、画像、連絡先情報といった一般的なデータタイプ用のContent Providerが多く含まれています。AndroidのネイティブなContent Providerの一例がproviderパッケージ内にありますのでご参照ください。

Content Providerがデータをどのように処理するか、という点についてはそれぞれの実装に依存しますが、全てのContent Providerが従うべきデータ要求の受け入れ方と結果の返し方についての規約がります。しかしながら、Content Provider側ではその公開する情報に応じた、データ保存・取得を単純化する独自のヘルパ関数を実装することも出来ます。

本稿はContent Providerに関する以下の2つのトピックについて書かれています。

Content Providerを使ってデータを読み書きする

このセクションでは自身で実装した、あるいは他の開発者により実装された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ではBaseColumnsPhonesColumnsPeopleColumnsを拡張しています)か、あるいはカラム名群を定数としてリストする方法かのいずれかの方法により、どのようなカラムが指定可能であるかを公開する必要があります。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を作成する方法を以下に示します。

  1. ContentProviderの拡張クラスを作成します。
  2. public static final ContentURIのCONTENT_URIを定義します。これには、これから作成するContent Providerが処理対象とする完全な”content://”のURIを書き込みます。この値はユニークにする必要がありますので、通常はContent Providerの完全クラス名を小文字にしたものを利用するのが良いでしょう。例えば以下のようになります。
    public static final ContentURI CONTENT_URI = ContentURI.create( "content://com.google.codelab.rssprovider");
  3. データ保存を行うシステムを書きます。ほとんどのContent ProviderはデータをAndroidのファイルストレージメソッドかSQLiteデータベースを用いて保存しますが、データやり取りの規約に従っている範疇においては、どのような保存方法をとっても構いません。
  4. クライアントに返すカラムのリストを定義します。データ保存場所にデータベースを利用している場合、から無名は通常SQLデータベースにおけるカラム名と同じものとなります。またこの時、レコード番号を特定するための_idという整数値カラムが必要です。SQLiteデータベースを利用している場合、このカラムのタイプはINTEGER PRIMARY KEY AUTOINCREMENTとなるのが普通です。AUTOINCREMENT指定は任意ですが、この指定をかけてもかけなくてもSQLiteはデフォルトでIDカウンタフィールドの値に、テーブル内でついている最大値よりも大きな値を付けます。このとき、テーブル内にある最後の行を削除すると、次に行追加を行った場合追加された行は削除された行と同じIDを持つことになります。このような事態を防ぐためには、IDカラムの定義をINTEGER PRIMARY KEY AUTOINCREMENTとします(他のフィールド(例えばURL)の値がテーブル内でユニークだったとしても、_idフィールドは必要です)。Androidではデータベースへのデータ追加や管理を[※13] 楽にしてくれるContentProviderDatabaseHelperクラスを提供しています。
  5. ビットマップファイルなどのバイトデータを公開する場合、データベース内にはファイルを指定するcontent://のURIを記入するのが普通です。読込先のコンテンツタイプ(これは読み込み元のものと同じ場合、違う場合が共に考えられます。例えば写真を保存していれば、mediaContent Providerを利用することになります)は、_dataレコードを実装する必要があります。_dataフィールドはデバイス内で指定されたファイルを特定するためのファイルパスとなります[※14] 。このフィールドはクライアントから読み込むことを想定したものではなく、ContentResolverにより読み込むためのものです。クライアント側では、アイテムのURIを付けてContentResolver.openOutputStream()を呼び出します(例えば、photoと名付けられたカラムにはcontent://media/images/4453という文字列が入っているかもしれません)。ContentResolver側ではレコードの_dataフィールドを要求し、クライアントよりも高い許可情報を持っているのでファイルに直接アクセス出来、クライアントに対してファイル読み込みラッパを返します。
  6. クライアントが読み込み対象カラム指定をかけたり、カーソルから読み出すフィールド指定をかけたりするためにカラムごとのpublic static Stringを定義しておく必要があります[※15] 。また、各フィールドについてのドキュメントを正確に書く必要があります。繰り返しになりますが、音声ファイルやビットマップファイルのフィールドからは通常パス文字列が返されることに注意して下さい。
  7. クエリへの応答時に、レコードセットのCursorオブジェクトを返します。つまり、query()、update()、insert()、delete()メソッドを実装するということになります[※16] 。この時、ContentResolver.notifyChange()を呼び出してリスナに対して更新された情報についての通知を行うことも出来ます。
  8. AndroidManifest.xmlに<provider>タグを追加し、その中にauthorities 属性を付け、自身の処理するコンテンツタイプについての管理情報[※17] を書き込みます。例えば、作成したContent Providerが車一覧を返すcontent://com.example.autos/autoである場合、authoritiesはcom.example.autosとなるでしょう。multiprocess属性は、複数のContent Providerが同時に動作しても問題ない場合のみtrueに設定して下さい。
  9. 新たなデータタイプを処理する場合は、必ずandroid.ContentProvider.getType(url)で必要となる[※18] MIMEタイプを定義する必要があります。このタイプはContent Providerにより処理されるコンテンツタイプのうちの一つで、getType()に渡されるものと一致します。[※19] 各コンテンツタイプ用のMIMEタイプ宣言にはそれぞれのレコード用のものと、複数レコード用のものの2つがあります。ContentURIのメソッドを使うことでどちらが要求されているかを判別することが出来ます。以下にそれぞれの一般的なフォーマットを示します * 単一レコードにはvnd.<会社名>.cursor.item/<コンテンツタイプ>を利用します。例えばIDが122の列車を取得するためのcontent://com.example.transportationprovider/trains/122を用いてのリクエストに対しては、vnd.example.cursor.item/railというMIMEタイプを返すことになるでしょう。 * 複数レコードにはvnd.<会社名>.cursor.dir/<コンテンツタイプ>を利用します。つまり、全ての列車レコードを取得するためのcontent://com.example.transportationprovider/trains を用いてのリクエストに対しては、vnd.example.cursor.dir/rail というMIMEタイプを返すことになるでしょう。

独自のContent Provider例については、SDKに含まれているメモ帳内のNotePadProviderを参照して下さい。

最後に、コンテンツURIの重要な部分について再度まとめておきます。

Elements of a content URI
  1. 標準のプリフィックスで、変更されることはありません。
  2. プロバイダ指定部分です。サードパーティアプリケーションにおいては、ユニーク性を確保するために、Content Providerの完全なクラス名を記入して下さい。この値は、<provider>要素内のauthorities 属性に指定したものと一致します。例えば<provider class="TransportationProvider" authorities="com.example.transportationprovider" />という具合です。
  3. Content Provider側でクライアントがどのようなデータを求めているかを解釈するためのパスです。この部分は0個以上のセグメントで構成されます。Content Providerがある1つのデータタイプのみを公開する場合(例えば『列車』という情報のみ)、この部分は省略出来ます。逆にContent Providerがサブタイプを含む複数のデータタイプを提供する、例えば”land/bus、land/train、sea/ship、sea/submarine“といった場合も考えられます。
  4. 必要に応じて付加される、特定のレコードを要求するための部分です。ここで渡すのは要求対象の_id値となります。特定タイプの全レコードが欲しい場合は、以下のようにスラッシュを含めてこの数値を省略します。content://com.example.transportationprovider/trains