內容供應器基本概念

內容供應器負責管理資料中央存放區的存取權。供應器是 Android 應用程式的一部分,通常會提供用於處理資料的專屬 UI。不過,內容供應器主要由其他應用程式使用,並透過供應器用戶端物件存取供應器。供應商和供應商用戶端就能攜手合作,為資料提供一致的標準介面,同時處理處理序間通訊,並確保資料存取安全。

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

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

本主題說明下列項目:

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

總覽

內容供應器會以一或多個資料表的形式,將資料提供給外部應用程式,如同關聯資料庫中的資料表。資料列代表供應商收集的某種資料例項,資料列中的每一欄都代表針對執行個體收集的個別資料。

內容供應器會協調多項不同 API 和元件對應用程式資料儲存層的存取權。如圖 1 所示,其中包含以下內容:

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

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

存取供應商

如要存取內容供應器中的資料,您可以使用應用程式 Context 中的 ContentResolver 物件,以用戶端的身分與供應器進行通訊。ContentResolver 物件會與供應器物件 (實作 ContentProvider 的類別例項) 進行通訊。

提供者物件會接收用戶端傳送的資料要求、執行要求的動作,並傳回結果。這個物件包含呼叫供應器物件中名稱相同方法的方法,即 ContentProvider 具體子類別的例項。ContentResolver 方法提供永久儲存空間的基本「CRUD」(建立、擷取、更新及刪除) 功能。

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

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

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

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

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

Android 平台的內建供應商是「使用者字典供應商」,可儲存使用者想要保留的非標準字詞。表 1 說明資料如這個供應器資料表的呈現方式:

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

字詞 應用程式編號 頻率 語言代碼 _ID
mapreduce 使用者 1 100 分 zh_TW 1
precompiler 使用者 14 200 fr_FR 2
applet 使用者 2 225 fr_CA 3
const 使用者 1 255 pt_BR 4
int 使用者 5 100 分 zh_TW 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: 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 包含整個供應器的符號名稱 (其「主機名稱」) 以及指向資料表的名稱「路徑」。當您呼叫用戶端方法以存取供應器中的資料表時,資料表的內容 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 類別,詳情請參閱 載入器指南。此外,這行程式碼只是程式碼片段。並不會顯示完整的應用程式。

如要從供應器擷取資料,請按照以下基本步驟操作:

  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.getCount() 為 0 (也就是空白遊標) 的 Cursor 物件。

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

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

下列程式碼片段接續了前一個程式碼片段的程式碼。其會建立包含查詢所擷取 CursorSimpleCursorAdapter 物件,並將此物件設為 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() 方法,可傳回代表資料欄資料類型的值。

內容供應器權限

供應商的應用程式可以指定其他應用程式存取供應器資料所需的權限。這類權限可讓使用者知道應用程式要存取哪些資料。根據供應商的要求,其他應用程式會要求存取供應器所需的權限。系統會在使用者安裝應用程式時,顯示要求的權限。

如果供應器的應用程式未指定任何權限,其他應用程式就無法存取供應器的資料 (除非已匯出供應器)。此外,無論指定的權限為何,供應器應用程式中的元件一律具備完整的讀取和寫入權限。

使用者字典提供者需要 android.permission.READ_USER_DICTIONARY 權限,才能從字典擷取資料。供應器有獨立的 android.permission.WRITE_USER_DICTIONARY 權限,可插入、更新或刪除資料。

應用程式如要取得存取供應器所需的權限,應用程式會在資訊清單檔案中以 <uses-permission> 元素要求這些權限。當 Android 套件管理員安裝應用程式時,使用者必須核准應用程式要求的所有權限。如果使用者核准要求,套件管理員會繼續安裝。如果使用者未核准要求,套件管理員會停止安裝。

下列 <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() 時清除使用者輸入內容。如要進一步瞭解相關資訊,請參閱「防範惡意輸入」一節。

供應商資料類型

內容供應器可以提供許多不同的資料類型。使用者字典供應商只能提供文字,但提供者也可以提供以下格式:

  • 整數
  • 長整數 (長)
  • 浮點
  • 長浮點 (雙精度浮點數)

供應商常用的另一種資料類型,是以 64 KB 位元組陣列實作的二進位大型物件 (BLOB)。您可以查看 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 本身。

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

  • 讀取或寫入 URI 代表的內容資料 (視意圖中的旗標而定)。
  • 針對含有與 URI 授權相符的內容供應器的應用程式,取得套件瀏覽權限。傳送意圖的應用程式和包含內容供應器的應用程式,可能是兩個不同的應用程式。

供應器使用 <provider> 元素的 android:grantUriPermissions 屬性以及 <provider> 元素的 <grant-uri-permission> 子項元素,定義資訊清單中的內容 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() 方法。這個方法會接收「 People」應用程式中的選取活動建立的結果意圖。
  5. 使用結果意圖的內容 URI 時,即使並未在資訊清單中要求供應器的永久讀取權限,您還是可以從聯絡人供應程式讀取聯絡人的資料。之後,您就能取得聯絡人的生日資訊或電子郵件地址,然後傳送電子問候語。

使用其他應用程式

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

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

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

如果您的應用程式「確實」具有存取權限,您仍可使用意圖在其他應用程式中顯示資料。舉例來說,Google 日曆應用程式接受 ACTION_VIEW 意圖顯示特定日期或活動。如此一來,您無須建立自己的 UI 也能顯示日曆資訊。如要進一步瞭解這項功能,請參閱「日曆供應商總覽」。

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

合約類別

合約類別定義了常數,可協助應用程式與內容 URI、資料欄名稱、意圖動作及內容供應器的其他功能搭配使用。供應商不會自動納入合約類別。供應器的開發人員必須定義這些 API,然後將內容提供給其他開發人員。Android 平台提供的許多供應商在 android.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。回應表 Line1 的下列內容 URI:

content://com.example.trains/Line1

供應器會傳回下列 MIME 類型:

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

回應表格第 2 列的下列內容 URI:

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

供應器會傳回下列 MIME 類型:

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

大多數內容供應器會針對他們使用的 MIME 類型定義合約類別常數,舉例來說,聯絡人提供者合約類別 ContactsContract.RawContacts 會針對單一原始聯絡人資料列的 MIME 類型定義常數 CONTENT_ITEM_TYPE

如需單一資料列的內容 URI,請參閱「內容 URI」一節。