內容供應器基本概念

內容供應器會管理資料中央存放區的存取權。提供者是 Android 應用程式的一部分,通常會提供專屬的 UI 來處理資料。不過,內容供應器主要由其他應用程式使用,這些應用程式會使用供應器用戶端物件存取供應器。供應器和供應器用戶端會共同提供一致的標準資料介面,同時處理跨程序通訊和安全資料存取。

一般來說,您在使用內容供應器時會採用其中一種:實作程式碼來存取其他應用程式中現有的內容供應器,或是在應用程式中建立新的內容供應器,以便與其他應用程式共用資料。

本頁說明與現有內容供應器合作的基本概念。如要瞭解如何在您自己的應用程式中實作內容供應器,請參閱「 建立內容供應器」。

本主題會說明下列內容:

  • 內容供應器的運作方式。
  • 您用於從內容供應器擷取資料的 API。
  • 您用來在內容供應器中插入、更新或刪除資料的 API。
  • 其他可協助與供應商合作的 API 功能。

總覽

內容供應器會將資料以一或多個表格的形式呈現給外部應用程式,這些表格類似於關聯資料庫中的表格。每列代表供應器收集的某種資料類型的例項,而每列中的每個欄代表為某個例項收集的個別資料。

內容供應器會協調應用程式中多個不同 API 和元件的資料儲存層存取權。如圖 1 所示,這些項目包括:

內容供應器和其他元件之間的關係。

圖 1. 內容供應器和其他元件之間的關係。

存取供應者

當您要存取內容供應器中的資料時,可使用應用程式 Context 中的 ContentResolver 物件,以用戶端的形式與供應器進行通訊。ContentResolver 物件會與提供者物件 (實作 ContentProvider 的類別執行個體) 通訊。

提供者物件會接收來自用戶端的資料要求、執行所要求的動作,然後傳回結果。這個物件含有方法,可在提供者物件 (ContentProvider 其中一個具體子類別的例項) 中呼叫同名方法。ContentResolver 方法提供永久儲存空間的基本「CRUD」函式 (建立、擷取、更新及刪除) 函式。

從 UI 存取 ContentProvider 的常見模式是使用 CursorLoader 在背景執行非同步查詢。UI 中的 ActivityFragment 會呼叫查詢的 CursorLoader,而後者會使用 ContentResolver 取得 ContentProvider

這樣一來,使用者在查詢執行期間仍可繼續使用 UI。這個模式涉及多種不同物件的互動,以及基礎儲存機制,如圖 2 所示。

ContentProvider、其他類別和儲存空間之間的互動。

圖 2. ContentProvider、其他類別和儲存空間之間的互動。

注意:如要存取提供者,應用程式通常必須在資訊清單檔案中要求特定權限。如要進一步瞭解這類開發模式,請參閱「內容提供者權限」一節。

使用者字典提供者是 Android 平台內建的其中一個供應器,這個提供者會儲存使用者想要保留的非標準字詞。表格 1 說明資料在這個供應商的資料表中可能的樣貌:

表 1:使用者字典資料表範例。

文字 應用程式編號 頻率 語言代碼 _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet 使用者 2 225 fr_CA 3
const user1 255 pt_BR 4
int 使用者 5 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

表 2 顯示 query(Uri,projection,selection,selectionArgs,sortOrder) 的引數如何與 SQL SELECT 陳述式相符:

表 2:與 SQL 查詢相比的 query()

query() 引數 選取關鍵字/參數 附註
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 包含整個供應器的符號名稱 (其主機名稱,以及指向資料表的名稱),即「路徑」。當您呼叫用戶端方法來存取供應器中的資料表時,資料表的內容 URI 會是其中一個引數。

在上述程式碼行中,常數 CONTENT_URI 包含使用者字典供應器的 Words 資料表內容 URI。ContentResolver 物件會剖析 URI 授權,並透過比對授權和已知供應器的系統表格來解析供應器。ContentResolver 接著可以將查詢引數調度至正確的提供者。

ContentProvider 會使用內容 URI 的路徑部分,選擇要存取的表格。供應器通常會為每個公開的資料表提供路徑。

在前面幾行程式碼中,Words 資料表的完整 URI 為:

content://user_dictionary/words
  • content:// 字串是「配置」,一律會顯示,並將此字串識別為內容 URI。
  • user_dictionary 字串是提供者的授權。
  • words 字串是資料表的路徑。

許多供應商會在 URI 結尾附加 ID 值,讓您存取資料表中的單一資料列。舉例來說,如要從使用者字典供應器擷取 _ID4 的資料列,您可以使用以下內容 URI:

Kotlin

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

Java

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

當您擷取一組資料列,然後想要更新或刪除其中一個資料列時,通常會使用 ID 值。

注意:UriUri.Builder 類別包含方便方法,可從字串建構格式正確的 URI 物件。ContentUris 類別包含方便方法,可將 ID 值附加至 URI。先前的程式碼片段使用 withAppendedId(),將 ID 附加至使用者字典提供者內容 URI。

從供應者擷取資料

本節將以使用者字典提供者為例,說明如何從提供者擷取資料。

為求清楚起見,本節的程式碼片段會在 UI 執行緒上呼叫 ContentResolver.query()。不過,在實際程式碼中,請在個別執行緒上以非同步方式執行查詢。您可以使用 CursorLoader 類別,詳情請參閱「 Loaders」指南。此外,這幾行程式碼只是程式碼片段,但無法顯示完整的應用程式。

從供應器擷取資料的基本步驟如下:

  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 陳述式,因此除非提供者已設定為擷取 SQL 注入嘗試,否則這可能會導致提供者刪除基礎 SQLite 資料庫中的所有資料表。

如要避免這個問題,請使用選取子句,並將 ? 做為可替換參數和獨立選取引數陣列。這樣一來,使用者輸入內容就會直接繫結至查詢,而不會解讀為 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 物件,其中 Cursor.getCount() 為 0,也就是空白游標。

如果發生內部錯誤,查詢結果將視特定供應商而定。它可能會傳回 null,也可能會擲回 Exception

由於 Cursor 是資料列清單,因此顯示 Cursor 內容的最佳方式,就是使用 SimpleCursorAdapter 將其連結至 ListView

下列程式碼片段接續上一個程式碼片段的程式碼。它會建立 SimpleCursorAdapter 物件,其中包含查詢擷取的 Cursor,並將此物件設為 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 就會停止安裝。

以下範例 <uses-permission> 元素會要求存取使用者字典供應器的讀取權限:

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

如要進一步瞭解權限對提供者存取權的影響,請參閱安全性提示

插入、更新及刪除資料

您可以透過與提供者用戶端和提供者的 ContentProvider 之間的互動,修改資料,這與從提供者擷取資料的方式相同。您可以使用引數呼叫 ContentResolver 的對應方法,這些引數會傳遞至 ContentProvider 的對應方法。供應器和供應器用戶端會自動處理安全性和跨程序通訊。

插入資料

如要將資料插入供應器,請呼叫 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
);

新資料列的資料會進入單一 ContentValues 物件,其格式類似於單列游標。這個物件中的資料欄不需要使用相同的資料類型,而且您完全不想指定值,可以使用 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)

供應商經常使用的另一種資料類型是二進位大型物件 (BLOB),實作方式為 64 KB 位元組陣列。您可以查看 Cursor 類別的「get」方法,瞭解可用的資料類型。

供應商的每個資料欄資料類型通常會列在說明文件中。 使用者字典供應器的資料類型列於其合約類別 UserDictionary.Words 的參考說明文件中。如需合約類別的說明,請參閱「合約類別」一節。您也可以呼叫 Cursor.getType() 來判斷資料類型。

供應器也會為所定義的每個內容 URI 維護 MIME 資料類型資訊。您可以使用 MIME 類型資訊來瞭解應用程式是否可以處理供應器提供的資料,或根據 MIME 類型選擇處理類型。當您與含有複雜資料結構或檔案的供應器合作時,通常需要使用 MIME 類型。

舉例來說,聯絡人供應程式中的 ContactsContract.Data 表格會使用 MIME 類型來為各資料列儲存的聯絡人資料類型加上標籤。如要取得與內容 URI 相對應的 MIME 類型,請呼叫 ContentResolver.getType()

MIME 類型參考資料一節會說明標準和自訂 MIME 類型的語法。

其他形式的供應者存取權

在應用程式開發中,有三種替代形式的供應器存取權相當重要:

以下各節將說明如何使用意圖進行批次存取和修改。

批次存取

批次存取提供者可用於插入大量資料列、在同一個方法呼叫中插入多個資料表的資料列,以及一般用於在跨程序邊界中以交易形式執行一組作業 (稱為「原子作業」)。

如要以批次模式存取供應器,請建立 ContentProviderOperation 物件的陣列,然後使用 ContentResolver.applyBatch() 將其調派至內容供應器。您必須將內容供應器的「主機名稱」傳遞至這個方法,而非特定內容 URI。

如此一來,陣列中的每個 ContentProviderOperation 物件都能針對不同的資料表運作。呼叫 ContentResolver.applyBatch() 會傳回結果陣列。

ContactsContract.RawContacts 合約類別的說明包含示範批次插入作業的程式碼片段。

使用意圖存取資料

意圖可間接存取內容供應器。即使應用程式沒有存取權限,您也可以讓使用者存取提供者中的資料,方法是從具有權限的應用程式取得結果意圖,或是啟用具有權限的應用程式,並讓使用者在該應用程式中執行作業。

使用臨時權限取得存取權

即使您沒有適當的存取權限,也可以存取內容供應器中的資料,方法是將意圖傳送至具有權限的應用程式,並接收含有 URI 權限的結果意圖。這些是特定內容 URI 的權限,會在接收這些權限的活動結束前持續有效。具有永久權限的應用程式會在結果意圖中設定標記,藉此授予臨時權限:

注意:這些標記不會將一般讀取或寫入權限授予權限包含在內容 URI 中的供應器。存取權僅限於 URI 本身。

將內容 URI 傳送至其他應用程式時,請至少加入其中一個旗標。這些標記可為接收意圖並指定 Android 11 (API 級別 30) 以上版本的任何應用程式提供下列功能:

  • 讀取或寫入 URI 代表的內容資料 (視意圖中包含的標記而定)。
  • 取得應用程式的套件可見度,該應用程式含有與 URI 授權相符的內容供應器。傳送意圖的應用程式以及包含內容供應器的應用程式可能是兩個不同的應用程式。

供應器會使用 <provider> 元素的 android:grantUriPermissions 屬性,以及 <provider> 元素的 <grant-uri-permission> 子項元素,在資訊清單中定義內容 URI 的 URI 權限。如要進一步瞭解 URI 權限機制,請參閱「Android 中的權限」指南。

舉例來說,即使您沒有 READ_CONTACTS 權限,也可以擷取聯絡資訊供應器中的聯絡人資料。您可能會在應用程式中執行這項操作,以便在聯絡人的生日當天傳送電子賀卡給他們。請不要要求 READ_CONTACTS,因為這會讓您存取使用者的所有聯絡人和所有資訊。請讓使用者自行控制應用程式使用的聯絡人。如要這麼做,請使用下列程序:

  1. 在應用程式中,使用 startActivityForResult() 方法傳送包含 ACTION_PICK 動作和「聯絡人」MIME 類型 CONTENT_ITEM_TYPE 的意圖。
  2. 由於這個意圖與 People 應用程式「selection」活動的意圖篩選器相符,因此活動會顯示在前景。
  3. 在選取活動中,使用者會選取要更新的聯絡人。在這種情況下,選取活動會呼叫 setResult(resultcode, intent) 來設定意圖,並將其傳回給應用程式。意圖包含使用者選取的聯絡人內容 URI 和「額外」標記 FLAG_GRANT_READ_URI_PERMISSION。這些旗標會授予應用程式 URI 權限,讓應用程式讀取內容 URI 所指向聯絡人的資料。選取活動會接著呼叫 finish(),將控制權還給應用程式。
  4. 活動會返回前景,系統會呼叫活動的 onActivityResult() 方法。此方法會接收「People」應用程式中所選活動建立的結果意圖。
  5. 即使您並未在資訊清單中要求提供者的永久讀取權限,也可以透過結果意圖的內容 URI 從聯絡人供應程式讀取聯絡人的資料。接著,您可以取得聯絡人的生日資訊或電子郵件地址,然後傳送電子賀卡。

使用其他應用程式

另一種讓使用者修改您沒有存取權限的資料的方法,是啟用具有權限的應用程式,讓使用者在該應用程式中執行工作。

舉例來說,日曆應用程式會接受 ACTION_INSERT 意圖,讓您啟用應用程式的插入 UI。您可以在這個意圖中傳遞「額外」資料,應用程式會使用這些資料預先填入 UI。由於週期性活動的語法相當複雜,因此建議您使用 ACTION_INSERT 啟用日曆應用程式,然後讓使用者在該處插入活動,以便將活動插入日曆供應器。

使用輔助應用程式顯示資料

如果應用程式存取權限,您還是可以使用意圖在其他應用程式中顯示資料。舉例來說,日曆應用程式會接受顯示特定日期或活動的 ACTION_VIEW 意圖。這樣一來,您不必建立自己的使用者介面,就能顯示日曆資訊。如要進一步瞭解這項功能,請參閱日曆供應器總覽

您傳送意圖的應用程式不必是與提供者相關聯的應用程式。舉例來說,您可以從聯絡人供應器擷取聯絡人,然後將包含聯絡人圖片內容 URI 的 ACTION_VIEW 意圖傳送至圖片檢視器。

合約課程

合約類別會定義常數,協助應用程式使用內容供應器的內容 URI、欄名稱、意圖動作和其他功能。供應商不會自動加入合約類別。供應者的開發人員必須定義這些值,然後提供給其他開發人員。Android 平台中包含的許多供應器,在 android.provider 套件中都有對應的合約類別。

舉例來說,User Dictionary Provider 有合約類別 UserDictionary,其中包含內容 URI 和欄位名稱常數。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。這個類別的參考說明文件包含程式碼片段範例。其中一個子類別 ContactsContract.Intents.Insert 是合約類別,內含意圖和意圖資料的常數。

MIME 類型參考資料

內容供應器可以傳回標準 MIME 媒體類型、自訂 MIME 類型字串,或兩者皆傳回。

MIME 類型格式如下:

type/subtype

舉例來說,眾所皆知的 MIME 類型 text/html 具有 text 類型和 html 子類型。如果供應器為 URI 傳回此類型,表示使用該 URI 的查詢會傳回包含 HTML 標記的文字。

自訂 MIME 類型字串 (又稱為「供應商專屬」MIME 類型) 擁有較複雜的 typesubtype 值。如果是多個資料列,類型值一律為下列值:

vnd.android.cursor.dir

對於單一資料列,類型值一律為:

vnd.android.cursor.item

subtype 為供應商專用功能,Android 內建供應商通常具有簡單的子類型。舉例來說,當通訊錄應用程式為電話號碼建立資料列時,會在該資料列中設定下列 MIME 類型:

vnd.android.cursor.item/phone_v2

子類型值為 phone_v2

其他供應器開發人員可以根據供應器的權威和表格名稱,建立自己的子類型模式。舉例來說,假設某個供應商提供火車時刻表。供應商的授權為 com.example.trains,內含 Line1、Line2 和 Line3 表格。回應第 1 行表的下列內容 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 類型定義合約類別常數。舉例來說,Contacts Provider 合約類別 ContactsContract.RawContacts 會定義單一原始聯絡資料列的 MIME 類型常數 CONTENT_ITEM_TYPE

單一資料列的內容 URI 說明請見「內容 URI」一節。