コンテンツ プロバイダの基本

コンテンツ プロバイダは、データのセントラル リポジトリへのアクセスを管理します。プロバイダは Android アプリの一部であり、多くの場合、データを操作する独自の UI を提供します。ただし、コンテンツ プロバイダは主に、プロバイダ クライアント オブジェクトを使用してプロバイダにアクセスする他のアプリで使用されます。また、プロバイダとプロバイダ クライアントはデータを扱うための一貫した標準的なインターフェースを提供し、プロセス間の通信と安全なデータアクセスも処理します。

コンテンツ プロバイダは通常、2 つのシナリオ(既存のコンテンツ プロバイダにアクセスするためのコードを別のアプリに実装する、または他のアプリとデータを共有するために新しいコンテンツ プロバイダを作成する)のいずれかで使用します。

このページでは、既存のコンテンツ プロバイダを使用する場合の基本について説明します。独自のアプリにコンテンツ プロバイダを実装する方法については、 コンテンツ プロバイダを作成するをご覧ください。

このトピックで説明する内容は次のとおりです。

  • コンテンツ プロバイダの仕組み。
  • コンテンツ プロバイダからのデータの取得に使用する API。
  • コンテンツ プロバイダへのデータの挿入、データの更新、または削除に使用する API。
  • プロバイダでの作業に役立つその他の API 機能。

概要

コンテンツ プロバイダは外部アプリに対し、リレーショナル データベースのテーブルに似た 1 つ以上のテーブルとしてデータを提供します。行はプロバイダが収集するなんらかのデータのインスタンスを表し、行の各列はインスタンスに対して収集した個々のデータを表します。

コンテンツ プロバイダは、さまざまな API やコンポーネントについてアプリのデータ ストレージ レイヤへのアクセスを調整します。図 1 に示すように、これには次のものが含まれます。

  • アプリデータへのアクセスを他のアプリと共有する
  • ウィジェットにデータを送信する
  • SearchRecentSuggestionsProvider を使用して、検索フレームワークを介してアプリのカスタム検索候補を返す
  • AbstractThreadedSyncAdapter の実装を使用してアプリデータをサーバーと同期する
  • CursorLoader を使用して UI にデータを読み込む
コンテンツ プロバイダと他のコンポーネントの関係。

図 1. コンテンツ プロバイダと他のコンポーネントとの関係。

プロバイダにアクセスする

コンテンツ プロバイダのデータにアクセスする場合は、アプリの ContextContentResolver オブジェクトを使用し、クライアントとしてプロバイダと通信します。ContentResolver オブジェクトは、ContentProvider を実装するクラスのインスタンスであるプロバイダ オブジェクトと通信します。

プロバイダ オブジェクトはクライアントからデータ リクエストを受け取り、リクエストされたアクションを実施して、結果を返します。このオブジェクトには、プロバイダ オブジェクト(ContentProvider の具象サブクラスの一つのインスタンス)内の同じ名前のメソッドを呼び出すメソッドがあります。ContentResolver メソッドは、永続ストレージの基本的な「CRUD」(作成、取得、更新、削除)機能を提供します。

UI から ContentProvider にアクセスするための一般的なパターンでは、CursorLoader を使用してバックグラウンドで非同期クエリを実行します。UI の Activity または Fragment はクエリに対して CursorLoader を呼び出し、次に ContentResolver を使用して ContentProvider を取得します。

こうして、ユーザーはクエリの実行中に引き続き UI を使用できます。このパターンには、図 2 に示すようにさまざまなオブジェクトとのやり取りと、基となるストレージ メカニズムが含まれます。

ContentProvider、他のクラス、ストレージの間のやり取り。

図 2. ContentProvider、他のクラス、ストレージの間のやり取り。

注: アプリがプロバイダにアクセスするには、通常、マニフェスト ファイルで特定の権限をリクエストする必要があります。この開発パターンについては、コンテンツ プロバイダの権限で詳しく説明します。

Android プラットフォームに組み込まれているプロバイダの 1 つに単語リストがありますが、ここにはユーザーが保存しておく標準以外の単語が格納されます。表 1 は、このプロバイダのテーブルにデータがどのように格納されるかを示しています。

表 1: 単語リストテーブルの例。

word app id frequency locale _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

表 1 の各行は、標準の単語リストにない単語のインスタンスを表しています。各列は、該当する単語のデータの一部(その単語が最初に見つかったロケールなど)を表しています。列の見出しは、プロバイダに格納される列の名前です。たとえば、ある行の言語 / 地域を参照するには、その locale 列を参照します。このプロバイダの場合、_ID 列が主キーの列として機能し、プロバイダはこの列を自動的に管理します。

単語リスト プロバイダから単語とその言語 / 地域のリストを取得するには、ContentResolver.query() を呼び出します。query() メソッドにより、単語リスト プロバイダが定義する ContentProvider.query() メソッドが呼び出されます。ContentResolver.query() 呼び出しを次のコード行に示します。

Kotlin

// Queries the UserDictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

Java

// Queries the UserDictionary and returns results
cursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    projection,                        // The columns to return for each row
    selectionClause,                   // Selection criteria
    selectionArgs,                     // Selection criteria
    sortOrder);                        // The sort order for the returned rows

query(Uri,projection,selection,selectionArgs,sortOrder) の引数と SQL SELECT ステートメントの対応関係を表 2 に示します。

表 2: query() と SQL クエリの比較。

query() 引数 SELECT キーワード / パラメータ 備考
Uri FROM table_name Uri は、table_name という名前のプロバイダのテーブルにマッピングされます。
projection col,col,col,... projection は、取得した各行に含まれる列の配列です。
selection WHERE col = value selection は、行を選択する際の基準を指定します。
selectionArgs 完全に一致するものはありません。選択引数は、選択句の ? プレースホルダを置き換えます。
sortOrder ORDER BY col,col,... sortOrder は、返される Cursor で行が表示される順序を指定します。

コンテンツ URI

コンテンツ URI は、プロバイダのデータを特定する URI です。コンテンツ URI には、プロバイダ全体のシンボリック名(オーソリティ)とテーブルを指す名前(パス)が含まれます。クライアント メソッドを呼び出してプロバイダのテーブルにアクセスする場合、引数のうち 1 つはテーブルのコンテンツ URI です。

上記のコード行では、定数 CONTENT_URI に、単語リスト プロバイダの Words テーブルのコンテンツ URI が含まれます。ContentResolver オブジェクトは URI のオーソリティをパースし、既知のプロバイダのシステム テーブルと比較して、プロバイダを解決します。そのため ContentResolver は、クエリ引数を正しいプロバイダにディスパッチできます。

ContentProvider は、アクセスするテーブルを選択するために、コンテンツ URI のパス部分を使用します。プロバイダには通常、公開する各テーブルのパスがあります。

上記のコード行では、Words テーブルの完全な URI は次のとおりです。

content://user_dictionary/words
  • content:// 文字列は常に存在するスキームで、コンテンツ URI として識別します。
  • user_dictionary 文字列はプロバイダの認証局です。
  • words 文字列はテーブルのパスです。

多くのプロバイダでは、ID 値を URI の末尾に追加することで、テーブル内の 1 つの行にアクセスできます。たとえば、_ID4 の行を単語リスト プロバイダから取得するには、次のコンテンツ URI を使用します。

Kotlin

val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)

Java

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

通常は、一連の行を取得してからいずれかの行を更新または削除するときに、ID 値を使用します。

注: Uri クラスと Uri.Builder クラスには、文字列から適切な形式の URI オブジェクトを作成するための便利なメソッドが用意されています。ContentUris クラスには、URI に ID 値を追加するための便利なメソッドが用意されています。上記のスニペットでは、withAppendedId() を使用してユーザー辞書プロバイダのコンテンツ URI に ID を追加しています。

プロバイダからデータを取得する

このセクションでは、単語リスト プロバイダの例を使い、プロバイダからデータを取得する方法を説明します。

わかりやすくするために、このセクションのコード スニペットは UI スレッドで ContentResolver.query() を呼び出しています。ただし、実際のコードでは、別のスレッドで非同期にクエリを実行します。CursorLoader クラスを使用できます。詳細については、 ローダのガイドをご覧ください。また、コード行はスニペットのみです。完全なアプリを示すものではありません。

プロバイダからデータを取得する基本的な手順は次のとおりです。

  1. プロバイダの読み取りアクセス権限をリクエストします。
  2. プロバイダにクエリを送信するコードを定義します。

読み取りアクセス権限をリクエストする

プロバイダからデータを取得するには、アプリにプロバイダの読み取りアクセス権限が必要です。この権限は実行時にリクエストできません。代わりに、<uses-permission> 要素とプロバイダが定義している正確な権限名を使用して、この権限が必要であることをマニフェストで指定する必要があります。

この要素をマニフェストで指定すると、アプリに対してこの権限をリクエストします。ユーザーがアプリをインストールすると、このリクエストが暗黙的に付与されます。

使用しているプロバイダの読み取りアクセス権限の正確な名前と、プロバイダで使用されている他のアクセス権限の名前を確認するには、プロバイダのドキュメントをご覧ください。

プロバイダにアクセスする際の権限の役割について詳しくは、コンテンツ プロバイダの権限のセクションをご覧ください。

単語リスト プロバイダはマニフェスト ファイルで権限 android.permission.READ_USER_DICTIONARY を定義するため、プロバイダからの読み取りを行うアプリは、この権限をリクエストする必要があります。

クエリを作成する

プロバイダからデータを取得する次のステップは、クエリの作成です。次のスニペットでは、単語リスト プロバイダにアクセスするための変数を定義しています。

Kotlin

// A "projection" defines the columns that are returned for each row
private val mProjection: Array<String> = arrayOf(
        UserDictionary.Words._ID,    // Contract class constant for the _ID column name
        UserDictionary.Words.WORD,   // Contract class constant for the word column name
        UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
)

// Defines a string to contain the selection clause
private var selectionClause: String? = null

// Declares an array to contain selection arguments
private lateinit var selectionArgs: Array<String>

Java

// A "projection" defines the columns that are returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String selectionClause = null;

// Initializes an array to contain selection arguments
String[] selectionArgs = {""};

次のスニペットでは、単語リスト プロバイダの例を使用して、ContentResolver.query() の使用方法を示しています。プロバイダ クライアント クエリは SQL クエリに似ており、返される列のセット、選択条件のセット、並べ替え順序を含みます。

クエリが返す列のセットを射影(変数 mProjection)といいます。

取得する行を指定する式は、選択句と選択引数に分割されます。選択句は、論理式、ブール式、列名、値の組み合わせです。変数は mSelectionClause です。値の代わりに置換可能なパラメータ ? を指定すると、クエリ メソッドは選択引数の配列(変数 mSelectionArgs)から値を取得します。

次のスニペットでは、ユーザーが単語を入力しない場合、選択句が null に設定され、クエリによってプロバイダのすべての単語が返されます。ユーザーが単語を入力すると、選択句が UserDictionary.Words.WORD + " = ?" に設定され、選択引数配列の最初の要素はユーザーが入力した単語に設定されます。

Kotlin

/*
 * This declares a String array to contain the selection arguments.
 */
private lateinit var selectionArgs: Array<String>

// Gets a word from the UI
searchString = searchWord.text.toString()

// Insert code here to check for invalid or malicious input

// If the word is the empty string, gets everything
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// Does a query against the table and returns a Cursor object
mCursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI, // The content URI of the words table
        projection,                       // The columns to return for each row
        selectionClause,                  // Either null or the word the user entered
        selectionArgs,                    // Either empty or the string the user entered
        sortOrder                         // The sort order for the returned rows
)

// Some providers return null if an error occurs, others throw an exception
when (mCursor?.count) {
    null -> {
        /*
         * Insert code here to handle the error. Be sure not to use the cursor!
         * You might want to call android.util.Log.e() to log this error.
         */
    }
    0 -> {
        /*
         * Insert code here to notify the user that the search is unsuccessful. This isn't
         * necessarily an error. You might want to offer the user the option to insert a new
         * row, or re-type the search term.
         */
    }
    else -> {
        // Insert code here to do something with the results
    }
}

Java

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] selectionArgs = {""};

// Gets a word from the UI
searchString = searchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(searchString)) {
    // Setting the selection clause to null returns all words
    selectionClause = null;
    selectionArgs[0] = "";

} else {
    // Constructs a selection clause that matches the word that the user entered
    selectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments
    selectionArgs[0] = searchString;

}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI, // The content URI of the words table
    projection,                       // The columns to return for each row
    selectionClause,                  // Either null or the word the user entered
    selectionArgs,                    // Either empty or the string the user entered
    sortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You can
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search is unsuccessful. This isn't necessarily
     * an error. You can offer the user the option to insert a new row, or re-type the
     * search term.
     */

} else {
    // Insert code here to do something with the results

}

このクエリは、次の SQL ステートメントに似ています。

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

この SQL ステートメントでは、コントラクト クラスの定数の代わりに実際の列名が使用されます。

悪意のある入力から保護する

コンテンツ プロバイダが管理するデータが SQL データベースにある場合、信頼できない外部のデータが未加工の SQL ステートメントに含まれていると、SQL インジェクションが発生する可能性があります。

次の選択句について考えてみましょう。

Kotlin

// Constructs a selection clause by concatenating the user's input to the column name
var selectionClause = "var = $mUserInput"

Java

// Constructs a selection clause by concatenating the user's input to the column name
String selectionClause = "var = " + userInput;

こうすると、ユーザーが悪意のある SQL を SQL ステートメントに連結できるようになります。たとえば、mUserInput に「nothing; DROP TABLE *;」と入力すると、選択句 var = nothing; DROP TABLE *; が生成されます。

選択句は SQL ステートメントとして扱われるため、プロバイダは基となる SQLite データベース内のすべてのテーブルを消去する可能性があります(プロバイダに SQL インジェクション試行の検出が設定されていない場合)。

この問題を回避するには、? を置換可能なパラメータとして使用する選択句と、選択引数の個別の配列を使用します。これにより、ユーザー入力は SQL ステートメントの一部として解釈されるのではなく、クエリに直接バインドされます。SQL として扱われないため、ユーザー入力によって悪意のある SQL が挿入されることはありません。ユーザー入力を含めるために連結を使用するのではなく、次の選択句を使用します。

Kotlin

// Constructs a selection clause with a replaceable parameter
var selectionClause = "var = ?"

Java

// Constructs a selection clause with a replaceable parameter
String selectionClause =  "var = ?";

選択引数の配列を次のように設定します。

Kotlin

// Defines a mutable list to contain the selection arguments
var selectionArgs: MutableList<String> = mutableListOf()

Java

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

選択引数の配列に次のように値を入力します。

Kotlin

// Adds the user's input to the selection argument
selectionArgs += userInput

Java

// Sets the selection argument to the user's input
selectionArgs[0] = userInput;

プロバイダが SQL データベースに基づいていない場合でも、? を置換可能なパラメータとして使用する選択句と、選択引数の配列を使用することをおすすめします。

クエリ結果を表示する

ContentResolver.query() クライアント メソッドは、常に Cursor を返します。これには、クエリの選択基準に一致する行のクエリの射影で指定される列が含まれます。Cursor オブジェクトは、含まれる行と列へのランダム読み取りアクセスを提供します。

Cursor メソッドを使用すると、結果の行の反復処理、各列のデータ型の決定、列からのデータの取得、結果のその他のプロパティの確認を行えます。

Cursor の実装によっては、プロバイダのデータが変更されたときにオブジェクトが自動で更新されるか、Cursor が変更されたときにオブザーバー オブジェクトのメソッドがトリガーされる、またはその両方が行われることがあります。

注: クエリを作成するオブジェクトの特性に基づいて、プロバイダによって列へのアクセスが制限されることがあります。たとえば、連絡先プロバイダによって同期アダプターは一部の列へのアクセスが制限されるため、アクティビティまたはサービスが返されません。

選択基準に一致する行がない場合、プロバイダは Cursor.getCount() が 0(空のカーソル)の Cursor オブジェクトを返します。

内部エラーが発生した場合、クエリの結果はプロバイダによって異なります。null が返される場合もあれば、Exception がスローされる場合もあります。

Cursor は行のリストであるため、Cursor のコンテンツを表示するには、SimpleCursorAdapter を使用して ListView にリンクすることをおすすめします。

次のスニペットは、前のスニペットのコードの続きです。クエリで取得した Cursor を含む SimpleCursorAdapter オブジェクトを作成し、このオブジェクトを ListView のアダプタとして設定します。

Kotlin

// Defines a list of columns to retrieve from the Cursor and load into an output row
val wordListColumns : Array<String> = arrayOf(
        UserDictionary.Words.WORD,      // Contract class constant containing the word column name
        UserDictionary.Words.LOCALE     // Contract class constant containing the locale column name
)

// Defines a list of View IDs that receive the Cursor columns for each row
val wordListItems = intArrayOf(R.id.dictWord, R.id.locale)

// Creates a new SimpleCursorAdapter
cursorAdapter = SimpleCursorAdapter(
        applicationContext,             // The application's Context object
        R.layout.wordlistrow,           // A layout in XML for one row in the ListView
        mCursor,                        // The result from the query
        wordListColumns,                // A string array of column names in the cursor
        wordListItems,                  // An integer array of view IDs in the row layout
        0                               // Flags (usually none are needed)
)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter)

Java

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] wordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that receive the Cursor columns for each row
int[] wordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
cursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    wordListColumns,                       // A string array of column names in the cursor
    wordListItems,                         // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter);

注: Cursor を使用して ListView を返すには、カーソルに _ID という名前の列を含める必要があります。このため、前述のクエリは Words テーブルの _ID 列を取得しますが、ListView には表示されません。また、この制限があることから、ほとんどのプロバイダは各テーブルに _ID 列を持ちます。

クエリ結果からデータを取得する

クエリ結果は、表示するだけでなく他のタスクにも使用できます。たとえば、単語リスト プロバイダからスペルを取得して、他のプロバイダで検索できます。これを行うには、次の例に示すように Cursor の行を反復処理します。

Kotlin

/*
* Only executes if the cursor is valid. The User Dictionary Provider returns null if
* an internal error occurs. Other providers might throw an Exception instead of returning null.
*/
mCursor?.apply {
    // Determine the column index of the column named "word"
    val index: Int = getColumnIndex(UserDictionary.Words.WORD)

    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you get an
     * exception.
     */
    while (moveToNext()) {
        // Gets the value from the column
        newWord = getString(index)

        // Insert code here to process the retrieved word
        ...
        // End of while loop
    }
}

Java

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers might throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word
        ...
        // End of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception
}

Cursor の実装には「get」メソッドがいくつか用意されており、オブジェクトからさまざまなタイプのデータを取得できます。たとえば上記のスニペットでは getString() を使用しています。また、列のデータ型を示す値を返す getType() メソッドも使用しています。

クエリ結果リソースを解放する

Cursor オブジェクトが不要になった場合は、関連付けられたリソースがすぐに解放されるように、閉じる必要があります。これは、close() を呼び出すか、Java プログラミング言語の try-with-resources ステートメントまたは Kotlin プログラミング言語の use() 関数を使用して行えます。

コンテンツ プロバイダの権限

プロバイダのアプリでは、他のアプリがプロバイダのデータにアクセスするために必要な権限を指定できます。これらの権限により、ユーザーはアプリがアクセスしようとしているデータを把握できます。他のアプリは、プロバイダの要件に基づき、プロバイダにアクセスするために必要な権限をリクエストします。エンドユーザーがアプリをインストールするとき、リクエストされた権限が表示されます。

プロバイダのアプリで権限が指定されていない場合、他のアプリはプロバイダのデータにアクセスできません(プロバイダがエクスポートされている場合を除く)。また、プロバイダのアプリのコンポーネントは、指定された権限に関係なく、常に完全な読み取りと書き込みのアクセス権を持ちます。

単語リスト プロバイダがデータを取得するには、android.permission.READ_USER_DICTIONARY 権限が必要です。プロバイダには、データの挿入、更新、削除について、個別の android.permission.WRITE_USER_DICTIONARY 権限があります。

プロバイダにアクセスするために必要な権限を取得するには、アプリのマニフェスト ファイルで <uses-permission> 要素を使用して権限をリクエストします。Android Package Manager でアプリをインストールするとき、ユーザーはアプリがリクエストするすべての権限を承認する必要があります。ユーザーが承認すると、Package Manager はインストールを続行します。ユーザーが承認しない場合、Package Manager はインストールを停止します。

次のサンプルの <uses-permission> 要素は、単語リスト プロバイダへの読み取りアクセス権をリクエストします。

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

プロバイダ アクセスに対する権限の影響については、セキュリティに関するヒントをご覧ください。

データを挿入、更新、削除する

プロバイダからデータを取得する場合と同じ方法で、プロバイダ クライアントとプロバイダの ContentProvider の間のやり取りを使用してデータを変更します。対応する ContentProvider のメソッドに渡す引数を使用して ContentResolver のメソッドを呼び出します。プロバイダとプロバイダ クライアントは、セキュリティとプロセス間通信を自動的に処理します。

データの挿入

プロバイダにデータを挿入するには、ContentResolver.insert() メソッドを呼び出します。このメソッドは、プロバイダに新しい行を挿入し、その行のコンテンツ URI を返します。次のスニペットは、単語リスト プロバイダに新しい単語を挿入する方法を示しています。

Kotlin

// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri
...
// Defines an object to contain the new values to insert
val newValues = ContentValues().apply {
    /*
     * Sets the values of each column and inserts the word. The arguments to the "put"
     * method are "column name" and "value".
     */
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   // The UserDictionary content URI
        newValues                           // The values to insert
)

Java

// Defines a new Uri object that receives the result of the insertion
Uri newUri;
...
// Defines an object to contain the new values to insert
ContentValues newValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value".
 */
newValues.put(UserDictionary.Words.APP_ID, "example.user");
newValues.put(UserDictionary.Words.LOCALE, "en_US");
newValues.put(UserDictionary.Words.WORD, "insert");
newValues.put(UserDictionary.Words.FREQUENCY, "100");

newUri = getContentResolver().insert(
    UserDictionary.Words.CONTENT_URI,   // The UserDictionary content URI
    newValues                           // The values to insert
);

新しい行のデータは 1 つの ContentValues オブジェクトに格納されます。これは 1 行カーソルの形式に似ています。このオブジェクトの列は同じデータ型である必要はありません。また、値をまったく指定しない場合は、ContentValues.putNull() を使用して列を null に設定できます。

この列は自動的に保持されるため、上記のスニペットは _ID 列を追加しません。プロバイダは、追加されるすべての行に _ID の一意の値を割り当てます。プロバイダは通常、この値をテーブルの主キーとして使用します。

newUri で返されるコンテンツ URI によって、新しく追加された行が次の形式で識別されます。

content://user_dictionary/words/<id_value>

<id_value> は、新しい行の _ID のコンテンツです。ほとんどのプロバイダは、この形式のコンテンツ URI を自動的に検出し、その特定の行に対してリクエストされたオペレーションを実施できます。

返された Uri から _ID の値を取得するには、ContentUris.parseId() を呼び出します。

データの更新

行を更新するには、挿入の場合と同様に値を更新した ContentValues オブジェクトと、クエリの場合と同様に選択基準を使用します。使用するクライアント メソッドは ContentResolver.update() です。更新する列の ContentValues オブジェクトに値を追加するだけで済みます。列のコンテンツを消去する場合は、値を null に設定します。

次のスニペットは、言語が "en" であるすべての行の言語 / 地域を null に変更します。戻り値は、更新された行の数です。

Kotlin

// Defines an object to contain the updated values
val updateValues = ContentValues().apply {
    /*
     * Sets the updated value and updates the selected words.
     */
    putNull(UserDictionary.Words.LOCALE)
}

// Defines selection criteria for the rows you want to update
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")

// Defines a variable to contain the number of updated rows
var rowsUpdated: Int = 0
...
rowsUpdated = contentResolver.update(
        UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
        updateValues,                      // The columns to update
        selectionClause,                   // The column to select on
        selectionArgs                      // The value to compare to
)

Java

// Defines an object to contain the updated values
ContentValues updateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String selectionClause = UserDictionary.Words.LOCALE +  " LIKE ?";
String[] selectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int rowsUpdated = 0;
...
/*
 * Sets the updated value and updates the selected words.
 */
updateValues.putNull(UserDictionary.Words.LOCALE);

rowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
    updateValues,                      // The columns to update
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

ContentResolver.update() を呼び出すときにユーザー入力をサニタイズします。詳細については、悪意のある入力から保護するをご覧ください。

データの削除

行の削除は行データの取得と似た操作になります。削除する行の選択基準を指定すると、削除した行数がクライアント メソッドから返されます。次のスニペットは、アプリ ID が "user" に一致する行を削除します。削除した行数がメソッドから返されます。

Kotlin

// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.APP_ID} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")

// Defines a variable to contain the number of rows deleted
var rowsDeleted: Int = 0
...
// Deletes the words that match the selection criteria
rowsDeleted = contentResolver.delete(
        UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
        selectionClause,                   // The column to select on
        selectionArgs                      // The value to compare to
)

Java

// Defines selection criteria for the rows you want to delete
String selectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] selectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int rowsDeleted = 0;
...
// Deletes the words that match the selection criteria
rowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

ContentResolver.delete() を呼び出すときにユーザー入力をサニタイズします。詳細については、悪意のある入力から保護するをご覧ください。

プロバイダのデータ型

コンテンツ プロバイダは、さまざまなデータ型を提供できます。単語リスト プロバイダはテキストのみを提供しますが、次の形式も提供できます。

  • 整数
  • 長整数(long)
  • 浮動小数点
  • 長精度浮動小数点(double)

データ型は他にも、64 KB バイト配列として実装されるバイナリラージ オブジェクト(BLOB)がプロバイダでよく使用されます。使用可能なデータ型は、Cursor クラスの「get」メソッドで確認できます。

プロバイダの各列のデータ型は通常、そのドキュメントに記載されています。単語リスト プロバイダのデータ型は、コントラクト クラス UserDictionary.Words のリファレンス ドキュメントに記載されています。コントラクト クラスについては、コントラクト クラスのセクションをご覧ください。Cursor.getType() を呼び出すことでもデータ型を判断できます。

プロバイダは、定義する各コンテンツ URI の MIME データ型情報も保持します。MIME タイプ情報を使用すると、プロバイダが提供するデータをアプリで処理できるかどうかを確認したり、MIME タイプに基づいて処理タイプを選択したりできます。通常、複雑なデータ構造やファイルを含むプロバイダを利用する場合は、MIME タイプが必要となります。

たとえば連絡先プロバイダの ContactsContract.Data テーブルでは、MIME タイプを使用して、各行に格納されている連絡先データのタイプにラベルを付けます。コンテンツ URI に対応する MIME タイプを取得するには、ContentResolver.getType() を呼び出します。

MIME タイプのリファレンス セクションでは、標準とカスタムの両方の MIME タイプの構文について説明しています。

別の形式のプロバイダ アクセス

アプリ開発では、次の 3 つの形式のプロバイダ アクセスが重要です。

以下のセクションでは、インテントを使用したバッチアクセスとバッチ変更について説明します。

バッチアクセス

プロバイダへのバッチアクセスは、多数の行を挿入する場合、同じメソッド呼び出しで複数のテーブルに行を挿入する場合、一般的には、プロセスの境界を越えて一連の操作をトランザクションとして行う場合に便利です(アトミック操作)。

バッチモードでプロバイダにアクセスするには、ContentProviderOperation オブジェクトの配列を作成し、ContentResolver.applyBatch() を使用してコンテンツ プロバイダにディスパッチします。このメソッドには、特定のコンテンツ URI ではなく、コンテンツ プロバイダのオーソリティを渡します。

これにより、配列内の各 ContentProviderOperation オブジェクトが異なるテーブルに対して機能できるようになります。ContentResolver.applyBatch() を呼び出すと、結果の配列が返されます。

ContactsContract.RawContacts コントラクト クラスの説明には、バッチ挿入を示すコード スニペットが含まれています。

インテントを使用したデータアクセス

インテントを使用すると、コンテンツ プロバイダに間接的にアクセスできます。アプリにアクセス権限がない場合でも、権限のあるアプリから結果のインテントを取得するか、権限のあるアプリをアクティベートしてユーザーに使用させることで、ユーザーはプロバイダのデータにアクセスできます。

一時的な権限でアクセス権を取得する

適切なアクセス権限がない場合でも、権限のあるアプリにインテントを送信し、URI 権限を含む結果のインテントを受け取ることで、コンテンツ プロバイダのデータにアクセスできます。これは特定のコンテンツ URI の権限であり、権限を受け取るアクティビティが終了するまで効力を持ちます。永続的な権限を持つアプリは、結果のインテントにフラグを設定することで、一時的な権限を付与します。

注: これらのフラグは、コンテンツ URI にオーソリティが含まれているプロバイダへの全般的な読み取りアクセス権または書き込みアクセス権を付与するものではありません。アクセスは URI 自体に限定されます。

コンテンツ URI を別のアプリに送信する場合は、これらのフラグのいずれかを 1 つ以上含める必要があります。このフラグにより、インテントを受け取り、Android 11(API レベル 30)以降をターゲットとするアプリに次の機能が提供されます。

  • インテントに含まれるフラグに応じて、コンテンツ URI が表すデータから読み取る、またはデータに書き込む。
  • URI オーソリティと一致するコンテンツ プロバイダを含むアプリのパッケージの可視性を取得します。インテントを送信するアプリとコンテンツ プロバイダを含むアプリは、2 つの異なるアプリである場合があります。

プロバイダは、<provider> 要素の android:grantUriPermissions 属性と <provider> 要素の <grant-uri-permission> 子要素を使用して、コンテンツ URI の URI 権限をマニフェストで定義します。URI 権限のメカニズムについて詳しくは、Android での権限ガイドをご覧ください。

たとえば、READ_CONTACTS 権限がない場合でも、連絡先プロバイダの連絡先データを取得できます。これは連絡先の誕生日にグリーティング メールを送信するアプリで利用できます。ユーザーのすべての連絡先と、そのすべての情報に対するアクセス権を付与する READ_CONTACTS をリクエストするのではなく、アプリで使用する連絡先をユーザーが管理できるようにします。そのためには、次の手順を使用します。

  1. アプリで、メソッド startActivityForResult() を使用して、アクション ACTION_PICK と「連絡先」の MIME タイプ CONTENT_ITEM_TYPE を含むインテントを送信します。
  2. このインテントは連絡帳アプリの「選択」アクティビティのインテント フィルタに一致するため、アクティビティがフォアグラウンドに移動します。
  3. 選択アクティビティで、更新する連絡先をユーザーが選択します。この場合、選択アクティビティは setResult(resultcode, intent) を呼び出して、アプリに返すインテントを設定します。インテントには、ユーザーが選択した連絡先のコンテンツ URI と「追加」のフラグ FLAG_GRANT_READ_URI_PERMISSION が含まれます。これらのフラグにより、コンテンツ URI が指す連絡先のデータを読み取るための URI 権限がアプリに付与されます。その後、選択アクティビティは finish() を呼び出して、制御をアプリに返します。
  4. アクティビティがフォアグラウンドに戻り、システムがアクティビティの onActivityResult() メソッドを呼び出します。このメソッドは、連絡帳アプリの選択アクティビティによって作成された結果のインテントを受け取ります。
  5. 結果のインテントのコンテンツ URI を使用すると、マニフェストで永続的な読み取りアクセス権限をプロバイダにリクエストしていなくても、連絡先プロバイダから連絡先データを読み取ることができます。その後、連絡先の誕生日情報またはメールアドレスを取得し、グリーティング メールを送信できます。

別のアプリケーションを使用

アクセス権限のないデータをユーザーが変更できるようにするもう 1 つの方法は、権限のあるアプリを有効にして、ユーザーにそのアプリを使用してもらうという方法です。

たとえばカレンダー アプリは、アプリの挿入 UI を有効にできる ACTION_INSERT インテントを受け入れます。このインテントに「追加」のデータを渡すと、アプリはこのデータを使用して UI を事前入力します。定期的な予定は構文が複雑であるため、カレンダー プロバイダにイベントを挿入する場合は、ACTION_INSERT でカレンダー アプリを有効にしてからユーザーにイベントを挿入してもらうことをおすすめします。

ヘルパーアプリを使用してデータを表示する

アプリにアクセス権限がある場合でも、インテントを使用して別のアプリでデータを表示できます。たとえばカレンダー アプリは、特定の日付またはイベントを表示する ACTION_VIEW インテントを受け入れます。これにより、独自の UI を作成しなくてもカレンダー情報を表示できます。この機能の詳細については、カレンダー プロバイダの概要をご覧ください。

インテントを送信するアプリは、プロバイダに関連付けられたアプリである必要はありません。たとえば、連絡先プロバイダから連絡先を取得して、連絡先の画像のコンテンツ URI を含む ACTION_VIEW インテントを画像ビューアに送信できます。

コントラクト クラス

コントラクト クラスは、アプリでコンテンツ URI、列名、インテント アクション、コンテンツ プロバイダのその他の機能を利用する際に役立つ定数を定義します。コントラクト クラスは、プロバイダに自動的に含まれません。プロバイダのデベロッパーは、これらの変数を定義してから、他のデベロッパーが使用できるようにする必要があります。Android プラットフォームに含まれる多くプロバイダでは、対応するコントラクト クラスがパッケージ android.provider にあります。

たとえば単語リスト プロバイダには、コンテンツ URI と列名の定数を含むコントラクト クラス UserDictionary があります。Words テーブルのコンテンツ URI は、定数 UserDictionary.Words.CONTENT_URI で定義されます。また UserDictionary.Words クラスには、このガイドのスニペットの例で使用されている列名の定数も含まれています。たとえば、クエリの射影は次のように定義できます。

Kotlin

val projection : Array<String> = arrayOf(
        UserDictionary.Words._ID,
        UserDictionary.Words.WORD,
        UserDictionary.Words.LOCALE
)

Java

String[] projection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};

別のコントラクト クラスは、連絡先プロバイダの ContactsContract です。このクラスのリファレンス ドキュメントには、コード スニペットの例が記載されています。サブクラスの 1 つである ContactsContract.Intents.Insert は、インテントとインテント データの定数を含むコントラクト クラスです。

MIME タイプのリファレンス

コンテンツ プロバイダは、標準の MIME メディアタイプ、カスタム MIME タイプの文字列、またはその両方を返すことができます。

MIME タイプの形式は次のとおりです。

type/subtype

たとえば、よく利用される MIME タイプ text/html には、text タイプと html サブタイプがあります。プロバイダが URI に対してこのタイプを返す場合、その URI を使用するクエリは HTML タグを含むテキストを返すことを意味します。

「ベンダー固有」の MIME タイプとも呼ばれるカスタムの MIME タイプ文字列には、より複雑な type 値と subtype 値があります。複数行の場合、type 値は常に次のようになります。

vnd.android.cursor.dir

1 行の場合、type 値は常に次のようになります。

vnd.android.cursor.item

subtype はプロバイダ固有の値です。通常、Android 組み込みプロバイダは単純なサブタイプを使用します。たとえば連絡先アプリで電話番号の行を作成すると、その行に次の MIME タイプが設定されます。

vnd.android.cursor.item/phone_v2

サブタイプの値は phone_v2 です。

他のプロバイダ デベロッパーは、プロバイダのオーソリティとテーブル名に基づいて、サブタイプの独自のパターンを作成できます。たとえば、列車の時刻表を含むプロバイダについて考えてみます。プロバイダのオーソリティは com.example.trains であり、テーブル Line1、Line2、Line3 が含まれています。テーブル Line1 の次のコンテンツ URI に対して:

content://com.example.trains/Line1

プロバイダは次の MIME タイプを返します。

vnd.android.cursor.dir/vnd.example.line1

テーブル Line2 の行 5 に対する次のコンテンツ URI に応答します。

content://com.example.trains/Line2/5

プロバイダは次の MIME タイプを返します。

vnd.android.cursor.item/vnd.example.line2

ほとんどのコンテンツ プロバイダは、使用する MIME タイプのコントラクト クラス定数を定義します。たとえば連絡先プロバイダのコントラクト クラス ContactsContract.RawContacts は、1 つの未加工連絡先行の MIME タイプに定数 CONTENT_ITEM_TYPE を定義します。

単一行のコンテンツ URI については、コンテンツ URI セクションをご覧ください。