內容提供程式管理對結構化資料集的訪問。它們封裝資料,並提供用於定義資料安全性的機制。 內容提供程式是連線一個程式中的資料與另一個程式中執行的程式碼的標準介面。
如果您想要訪問內容提供程式中的資料,可以將應用的 Context 中的 ContentResolver 物件用作客戶端來與提供程式通訊。 ContentResolver 物件會與提供程式物件(即實現 ContentProvider 的類例項)通訊。 提供程式物件從客戶端接收資料請求,執行請求的操作並返回結果。
Android 本身包括的內容提供程式可管理音訊、視訊、影象和個人聯絡資訊等資料。
內容提供程式以一個或多個表的形式將資料呈現給外部應用。 行表示提供程式收集的某種資料型別的例項,行中的每個列表示為例項收集的每條資料。
Android 平臺的內建提供程式之一是使用者字典,它會儲存使用者想要儲存的非標準字詞的拼寫。
訪問提供程式
應用從具有 ContentResolver 客戶端物件的內容提供程式訪問資料。 此物件具有呼叫提供程式物件(ContentProvider 的某個具體子類的例項)中同名方法的方法。 ContentResolver 方法可提供持續儲存的基本“CRUD”(建立、檢索、更新和刪除)功能。
客戶端應用程式中的 ContentResolver 物件和擁有提供程式的應用中的 ContentProvider 物件可自動處理跨程式通訊。 ContentProvider 還可充當其資料儲存區和表格形式的資料外部顯示之間的抽象層。
例如,要從使用者字典提供程式中獲取字詞及其語言區域的列表,則需呼叫 ContentResolver.query()。 query() 方法會呼叫使用者字典提供程式所定義的 ContentProvider.query() 方法。 以下程式碼行顯示了 ContentResolver.query() 呼叫:
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The columns to return for each row
mSelectionClause // Selection criteria
mSelectionArgs, // Selection criteria
mSortOrder); // The sort order for the returned rows
複製程式碼
內容 URI
內容 URI 是用於在提供程式中標識資料的 URI。內容 URI 包括整個提供程式的符號名稱(其授權)和一個指向表的名稱(路徑)。
在前面的程式碼行中,常量 CONTENT_URI 包含使用者字典的“字詞”表的內容 URI。 ContentResolver 物件會分析出 URI 的授權,並通過將該授權與已知提供程式的系統表進行比較,來“解析”提供程式。 然後, ContentResolver 可以將查詢引數分派給正確的提供程式。
ContentProvider 使用內容 URI 的路徑部分來選擇要訪問的表。 提供程式通常會為其公開的每個表顯示一條路徑。
在前面的程式碼行中,“字詞”表的完整 URI 是:
content://user_dictionary/words
複製程式碼
其中,user_dictionary 字串是提供程式的授權,words 字串是表的路徑。 字串 content://(架構)始終顯示,並將此標識為內容 URI。
許多提供程式都允許您通過將 ID 值追加到 URI 末尾來訪問表中的單個行。 例如,要從使用者字典中檢索 _ID 為 4 的行,則可使用此內容 URI:
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
複製程式碼
在檢索到一組行後想要更新或刪除其中某一行時通常會用到 ID 值。
從提供程式檢索資料
要從提供程式中檢索資料,請按照以下基本步驟執行操作:
- 請求對提供程式的讀取訪問許可權。
- 定義將查詢傳送至提供程式的程式碼。
請求讀取訪問許可權
要從提供程式檢索資料,您的應需要具備對提供程式的“讀取訪問”許可權。 您無法在執行時請求此許可權;相反,您需要使用<uses-permission>
元素和提供程式定義的準確許可權名稱,在清單檔案中指明您需要此許可權。 在您的清單檔案中指定此元素後,您將有效地為應用“請求”此許可權。 使用者安裝您的應用時,會隱式授予允許此請求。
使用者字典提供程式在其清單檔案中定義了許可權 android.permission.READ_USER_DICTIONARY,因此希望從提供程式中進行讀取的應用必需請求此許可權。
構建查詢
第一個程式碼段定義某些用於訪問使用者字典提供程式的變數:
// A "projection" defines the columns that will be 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 mSelectionClause = null;
// Initializes an array to contain selection arguments
String[] mSelectionArgs = {""};
複製程式碼
下一個程式碼段以使用者字典提供程式為例,顯示瞭如何使用 ContentResolver.query()。 提供程式客戶端查詢與 SQL 查詢類似,並且包含一組要返回的列、一組選擇條件和排序順序。
查詢應該返回的列集被稱為投影(變數 mProjection)。
用於指定要檢索的行的表示式分割為選擇子句和選擇引數。 選擇子句是邏輯和布林表示式、列名稱和值(變數 mSelectionClause)的組合。 如果您指定了可替換引數 ? 而非值,則查詢方法會從選擇引數陣列(變數 mSelectionArgs)中檢索值。
在下一個程式碼段中,如果使用者未輸入字詞,則選擇子句將設定為 null,而且查詢會返回提供程式中的所有字詞。 如果使用者輸入了字詞,選擇子句將設定為 UserDictionary.Words.WORD + " = ?" 且選擇引數陣列的第一個元素將設定為使用者輸入的字詞。
/*
* This defines a one-element String array to contain the selection argument.
*/
String[] mSelectionArgs = {""};
// Gets a word from the UI
mSearchString = mSearchWord.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(mSearchString)) {
// Setting the selection clause to null will return all words
mSelectionClause = null;
mSelectionArgs[0] = "";
} else {
// Constructs a selection clause that matches the word that the user entered.
mSelectionClause = UserDictionary.Words.WORD + " = ?";
// Moves the user's input string to the selection arguments.
mSelectionArgs[0] = mSearchString;
}
// 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
mProjection, // The columns to return for each row
mSelectionClause // Either null, or the word the user entered
mSelectionArgs, // Either empty, or the string the user entered
mSortOrder); // 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 may want to
* 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 was unsuccessful. This isn't necessarily
* an error. You may 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
}
複製程式碼
此查詢與 SQL 語句相似:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
複製程式碼
顯示查詢結果
ContentResolver.query() 客戶端方法始終會返回符合以下條件的 Cursor:包含查詢的投影為匹配查詢選擇條件的行指定的列。 Cursor 物件為其包含的行和列提供隨機讀取訪問許可權。 通過使用 Cursor 方法,您可以迴圈訪問結果中的行、確定每個列的資料型別、從列中獲取資料,並檢查結果的其他屬性。 某些 Cursor 實現會在提供程式的資料發生更改時自動更新物件和/或在 Cursor 更改時觸發觀察程式物件中的方法。
// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns =
{
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 will receive the Cursor columns for each row
int[] mWordListItems = { R.id.dictWord, R.id.locale};
// Creates a new SimpleCursorAdapter
mCursorAdapter = 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
mWordListColumns, // A string array of column names in the cursor
mWordListItems, // An integer array of view IDs in the row layout
0); // Flags (usually none are needed)
// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);
複製程式碼
從查詢結果中獲取資料
// 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 may 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 will 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 實現包含多個用於從物件中檢索不同型別的資料的“獲取”方法。 例如,上一個程式碼段使用 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">
複製程式碼
插入、更新和刪除資料
插入資料
要將資料插入提供程式,可呼叫 ContentResolver.insert() 方法。此方法會在提供程式中插入新行併為該行返回內容 URI。 此程式碼段顯示如何將新字詞插入使用者字典提供程式:
// Defines a new Uri object that receives the result of the insertion
Uri mNewUri;
...
// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();
/*
* Sets the values of each column and inserts the word. The arguments to the "put"
* method are "column name" and "value"
*/
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");
mNewUri = getContentResolver().insert(
UserDictionary.Word.CONTENT_URI, // the user dictionary content URI
mNewValues // 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。 返回值是已更新的行數:
// Defines an object to contain the updated values
ContentValues mUpdateValues = new ContentValues();
// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE + "LIKE ?";
String[] mSelectionArgs = {"en_%"};
// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;
...
/*
* Sets the updated value and updates the selected words.
*/
mUpdateValues.putNull(UserDictionary.Words.LOCALE);
mRowsUpdated = getContentResolver().update(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
mUpdateValues // the columns to update
mSelectionClause // the column to select on
mSelectionArgs // the value to compare to
);
複製程式碼
刪除資料
刪除行與檢索行資料類似:為要刪除的行指定選擇條件,客戶端方法會返回已刪除的行數。 以下程式碼段會刪除應用 ID 與“使用者”匹配的行。該方法會返回已刪除的行數。
// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};
// Defines a variable to contain the number of rows deleted
int mRowsDeleted = 0;
...
// Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
mSelectionClause // the column to select on
mSelectionArgs // the value to compare to
);
複製程式碼
提供程式資料型別
內容提供程式可以提供多種不同的資料型別。使用者字典提供程式僅提供文字,但提供程式也能提供以下格式:
- 整型
- 長整型(長)
- 浮點型
- 長浮點型(雙倍)
提供程式經常使用的另一種資料型別是作為 64KB 位元組的陣列實施的二進位制大型物件 (BLOB)。
提供程式還會維護其定義的每個內容 URI 的 MIME(多用途網際網路郵件擴充套件)資料型別資訊。您可以使用 MIME 型別資訊查明應用是否可以處理提供程式提供的資料,或根據 MIME 型別選擇處理型別。 在使用包含複雜資料結構或檔案的提供程式時,通常需要 MIME 型別。 例如,聯絡人提供程式中的 ContactsContract.Data 表會使用 MIME 型別來標記每行中儲存的聯絡人資料型別。 要獲取與內容 URI 對應的 MIME 型別,請呼叫 ContentResolver.getType()。
MIME 型別引用
內容提供程式可以返回標準 MIME 媒體型別和/或自定義 MIME 型別字串。
MIME 型別具有格式
type/subtype
複製程式碼
眾所周知的 MIME 型別 text/html 具有 text 型別和 html 子型別。如果提供程式為 URI 返回此型別,則意味著使用該 URI 的查詢會返回包含 HTML 標記的文字。
自定義 MIME 型別字串(也稱為“特定於供應商”的 MIME 型別)具有更加複雜的型別和子型別值。 型別值始終為
vnd.android.cursor.dir
複製程式碼
(多行)或
vnd.android.cursor.item
複製程式碼
(單行)。
子型別特定於提供程式。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 會為單個原始聯絡人行的 MIME 型別定義常量 CONTENT_ITEM_TYPE。
提供程式訪問的替代形式
提供程式訪問的三種替代形式在應用開發過程中十分重要:
- 批量訪問:您可以通過 ContentProviderOperation 類中的方法建立一批訪問呼叫,然後通過 ContentResolver.applyBatch() 應用它們。
- 非同步查詢:您應該在單獨執行緒中執行查詢。執行此操作的方式之一是使用 CursorLoader 物件。 載入器指南中的示例展示瞭如何執行此操作。
- 通過 Intent 訪問資料:儘管您無法直接向提供程式傳送 Intent,但可以向提供程式的應用傳送 Intent,後者通常具有修改提供程式資料的最佳配置。
批量訪問
批量訪問提供程式適用於插入大量行,或通過同一方法呼叫在多個表中插入行,或者通常用於跨程式界限將一組操作作為事務處理(原子操作)執行。
要在“批量模式”下訪問提供程式, 您可以建立 ContentProviderOperation 物件陣列,然後使用 ContentResolver.applyBatch() 將其分派給內容提供程式。您需將內容提供程式的授權傳遞給此方法,而不是特定內容 URI。 這樣可使陣列中的每個 ContentProviderOperation 物件都能適用於其他表。 呼叫 ContentResolver.applyBatch() 會返回結果陣列。
ArrayList<ContentProviderOperation> ops =
new ArrayList<ContentProviderOperation>();
...
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_TYPE, accountType)
.withValue(RawContacts.ACCOUNT_NAME, accountName)
.build());
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, "Mike Sullivan")
.build());
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
複製程式碼
協定類
協定類定義幫助應用使用內容 URI、列名稱、 Intent 操作以及內容提供程式的其他功能的常量。 協定類未自動包含在提供程式中;提供程式的開發者需要定義它們,然後使其可用於其他開發者。
例如,使用者字典提供程式具有包含內容 URI 和列名稱常量的協定類 UserDictionary。 “字詞”表的內容 URI 在常量 UserDictionary.Words.CONTENT_URI 中定義。 UserDictionary.Words 類也包含列名稱常量,本指南的示例程式碼段中就使用了該常量。 例如,查詢投影可以定義為:
String[] mProjection =
{
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
複製程式碼
通過 Intent 訪問資料
Intent 可以提供對內容提供程式的間接訪問。即使您的應用不具備訪問許可權,您也可以通過以下方式允許使用者訪問提供程式中的資料:從具有許可權的應用中獲取回結果 Intent,或者通過啟用具有許可權的應用,然後讓使用者在其中工作。
通過臨時許可權獲取訪問許可權
即使您沒有適當的訪問許可權,也可以通過以下方式訪問內容提供程式中的資料:將 Intent 傳送至具有許可權的應用,然後接收回包含“URI”許可權的結果 Intent。 這些是特定內容 URI 的許可權,將持續至接收該許可權的 Activity 結束。 具有永久許可權的應用將通過在結果 Intent 中設定標誌來授予臨時許可權:
讀取許可權: FLAG_GRANT_READ_URI_PERMISSION
寫入許可權: FLAG_GRANT_WRITE_URI_PERMISSION
複製程式碼
提供程式使用 <provider>
元素的 android:grantUriPermission 屬性以及 <provider>
元素的 <grant-uri-permission>
子元素在其清單檔案中定義內容 URI 的 URI 許可權。
例如,即使您沒有 READ_CONTACTS 許可權,也可以在聯絡人提供程式中檢索聯絡人的資料。您可能希望在向聯絡人傳送電子生日祝福的應用中執行此操作。 您更願意讓使用者控制應用所使用的聯絡人,而不是請求 READ_CONTACTS,讓您能夠訪問使用者的所有聯絡人及其資訊。 要執行此操作,您需要使用以下程式:
- 您的應用會使用方法 startActivityForResult() 傳送包含操作 ACTION_PICK 和“聯絡人”MIME 型別 CONTENT_ITEM_TYPE 的 Intent 物件。
- 由於此 Intent 與“聯絡人”應用的“選擇” Activity 的 Intent 過濾器相匹配,因此 Activity 會顯示在前臺。
- 在選擇 Activity 中,使用者選擇要更新的聯絡人。 發生此情況時,選擇 Activity 會呼叫 setResult(resultcode, intent) 以設定用於返回至應用的 Intent。Intent 包含使用者選擇的聯絡人的內容 URI,以及“extra”標誌 FLAG_GRANT_READ_URI_PERMISSION。這些標誌會為您的應用授予讀取內容 URI 所指向聯絡人資料的 URI 許可權。 然後,選擇 Activity 會呼叫 finish(), 將控制權交還給您的應用。
- 您的 Activity 會返回至前臺,系統會呼叫您的 Activity 的 onActivityResult() 方法。此方法會收到“聯絡人”應用中選擇 Activity 所建立的結果 Intent。
- 通過來自結果 Intent 的內容 URI,您可以讀取來自聯絡人提供程式的聯絡人資料,即使您未在清單檔案中請求對該提供程式的永久讀取訪問許可權。 您可以獲取聯絡人的生日資訊或其電子郵件地址,然後傳送電子祝福。
使用其他應用
允許使用者修改您無權訪問的資料的簡單方法是啟用具有許可權的應用,讓使用者在其中執行工作。
例如,日曆應用接受 ACTION_INSERT Intent 物件,這讓您可以啟用 應用的插入 UI。您可以在此 Intent(應用將使用該 Intent 來預先填充 UI)中傳遞“extra”資料。 由於定期事件具有複雜的語法,因此將事件插入日曆提供程式的首選方式是啟用具有 ACTION_INSERT 的日曆應用,然後讓使用者在其中插入事件。
建立內容提供程式
- 為您的資料設計原始儲存。內容提供程式以兩種方式提供資料:
檔案資料
通常儲存在檔案中的資料,如照片、音訊或視訊。 將檔案儲存在您的應用的私有空間內。 您的提供程式可以應其他應用發出的檔案請求提供檔案控制程式碼。
“結構化”資料
通常儲存在資料庫、陣列或類似結構中的資料。 以相容行列表的形式儲存資料。行表示實體,如人員或庫存專案。 列表示實體的某項資料,如人員的姓名或商品的價格。 此類資料通常儲存在 SQLite 資料庫中,但您可以使用任何型別的持久儲存。
- 定義 ContentProvider 類及其所需方法的具體實現。 此類是您的資料與 Android 系統其餘部分之間的介面。
- 定義提供程式的授權字串、其內容 URI 以及列名稱。如果您想讓提供程式的應用處理 Intent,則還要定義 Intent 操作、Extra 資料以及標誌。 此外,還要定義想要訪問您的資料的應用必須具備的許可權。 您應該考慮在一個單獨的協定類中將所有這些值定義為常量;以後您可以將此類公開給其他開發者。
設計資料儲存
內容提供程式是用於訪問以結構化格式儲存的資料的介面。
以下是 Android 中提供的一些資料儲存技術:
Android 系統包括一個 SQLite 資料庫 API,Android 自己的提供程式使用它來儲存面向表的資料。 SQLiteOpenHelper 類可幫助您建立資料庫,SQLiteDatabase 類是用於訪問資料庫的基類。 請記住,您不必使用資料庫來實現儲存區。提供程式在外部表現為一組表,與關係型資料庫類似,但這並不是對提供程式內部實現的要求;
為了儲存檔案資料,Android 提供了各種面向檔案的 API。
要想使用基於網路的資料,請使用 java.net 和 android.net 中的類。
資料設計考慮事項
以下是一些設計提供程式資料結構的技巧:
表資料應始終具有一個“主鍵”列,提供程式將其作為與每行對應的唯一數字值加以維護。 您可以使用此值將該行連結到其他表中的相關行(將其用作“外來鍵”)。 儘管您可以為此列使用任何名稱 ,但使用 BaseColumns._ID 是最佳選擇,因為將提供程式查詢的結果連結到 ListView 的條件是,檢索到的其中一個列的名稱必須是 _ID;
如果您想提供點陣圖影象或其他非常龐大的檔案導向型資料,請將資料儲存在一個檔案中,然後間接提供這些資料,而不是直接將其儲存在表中。 如果您執行了此操作,則需要告知提供程式的使用者,他們需要使用 ContentResolver 檔案方法來訪問資料;
使用二進位制大型物件 (BLOB) 資料型別儲存大小或結構會發生變化的資料。 例如,您可以使用 BLOB 列來儲存協議緩衝區或 JSON 結構。
您也可以使用 BLOB 來實現獨立於架構的表。在這類表中,您需要以 BLOB 形式定義一個主鍵列、一個 MIME 型別列以及一個或多個通用列。 這些 BLOB 列中資料的含義通過 MIME 型別列中的值指示。 這樣一來,您就可以在同一個表中儲存不同型別的行。 舉例來說,聯絡人提供程式的“資料”表 ContactsContract.Data 便是一個獨立於架構的表。
設計內容 URI
內容 URI 是用於在提供程式中標識資料的 URI。內容 URI 包括整個提供程式的符號名稱(其授權)和一個指向表或檔案的名稱(路徑)。 可選 ID 部分指向表中的單個行。 ContentProvider 的每一個資料訪問方法都將內容 URI 作為引數;您可以利用這一點確定要訪問的表、行或檔案。
設計授權
提供程式通常具有單一授權,該授權充當其 Android 內部名稱。為避免與其他提供程式發生衝突,您應該使用網際網路網域所有權(反向)作為提供程式授權的基礎。 由於此建議也適用於 Android 軟體包名稱,因此您可以將提供程式授權定義為包含該提供程式的軟體包名稱的副檔名。 例如,如果您的 Android 軟體包名稱為 com.example.,則應為提供程式提供 com.example..provider 授權。
設計路徑結構
開發者通常通過追加指向單個表的路徑來根據許可權建立內容 URI。 例如,如果您有兩個表:table1 和 table2,則可以通過合併上一示例中的許可權來生成 內容 URI com.example..provider/table1 和 com.example..provider/table2。路徑並不限定於單個段,也無需為每一級路徑都建立一個表。
處理內容 URI ID
按照慣例,提供程式通過接受末尾具有行所對應 ID 值的內容 URI 來提供對錶中單個行的訪問。 同樣按照慣例,提供程式會將該 ID 值與表的 _ID 列進行匹配,並對匹配的行執行請求的訪問。
這一慣例為訪問提供程式的應用的常見設計模式提供了便利。應用會對提供程式執行查詢,並使用 CursorAdapter 以 ListView 顯示生成的 Cursor。 定義 CursorAdapter 的條件是, Cursor 中的其中一個列必須是 _ID
使用者隨後從 UI 上顯示的行中選取其中一行,以檢視或修改資料。 應用會從支援 ListView 的 Cursor 中獲取對應行,獲取該行的 _ID 值,將其追加到內容 URI,然後向提供程式傳送訪問請求。 然後,提供程式便可對使用者選取的特定行執行查詢或修改。
內容 URI 模式
為幫助您選擇對傳入的內容 URI 執行的操作,提供程式 API 加入了實用類 UriMatcher,它會將內容 URI“模式”對映到整型值。 您可以在一個 switch 語句中使用這些整型值,為匹配特定模式的一個或多個內容 URI 選擇所需操作。
內容 URI 模式使用萬用字元匹配內容 URI:
*:匹配由任意長度的任何有效字元組成的字串
#:匹配由任意長度的數字字元組成的字串
以設計和編碼內容 URI 處理為例,假設一個具有授權 com.example.app.provider 的提供程式能識別以下指向表的內容 URI:
content://com.example.app.provider/table1:一個名為 table1 的表 content://com.example.app.provider/table2/dataset1:一個名為 dataset1 的表 content://com.example.app.provider/table2/dataset2:一個名為 dataset2 的表 content://com.example.app.provider/table3:一個名為 table3 的表 提供程式也能識別追加了行 ID 的內容 URI,例如,content://com.example.app.provider/table3/1 對應由 table3 中 1 標識的行的內容 URI。
可以使用以下內容 URI 模式:
content://com.example.app.provider/*
匹配提供程式中的任何內容 URI。
content://com.example.app.provider/table2/*:
匹配表 dataset1 和表 dataset2 的內容 URI,但不匹配 table1 或 table3 的內容 URI。
content://com.example.app.provider/table3/#:匹配 table3 中單個行的內容 URI,如 content://com.example.app.provider/table3/6 對應由 6 標識的行的內容 URI。
以下程式碼段演示了 UriMatcher 中方法的工作方式。 此程式碼採用不同方式處理整個表的 URI 與單個行的 URI,它為表使用的內容 URI 模式是 content://<authority>/<path>
,為單個行使用的內容 URI 模式則是 content://<authority>/<path>/<id>
。
方法 addURI() 會將授權和路徑對映到一個整型值。 方法 match() 會返回 URI 的整型值。switch 語句會在查詢整個表與查詢單個記錄之間進行選擇:
public class ExampleProvider extends ContentProvider {
...
// Creates a UriMatcher object.
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
/*
* The calls to addURI() go here, for all of the content URI patterns that the provider
* should recognize. For this snippet, only the calls for table 3 are shown.
*/
/*
* Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
* in the path
*/
sUriMatcher.addURI("com.example.app.provider", "table3", 1);
/*
* Sets the code for a single row to 2. In this case, the "#" wildcard is
* used. "content://com.example.app.provider/table3/3" matches, but
* "content://com.example.app.provider/table3 doesn't.
*/
sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
}
...
// Implements ContentProvider.query()
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
...
/*
* Choose the table to query and a sort order based on the code returned for the incoming
* URI. Here, too, only the statements for table 3 are shown.
*/
switch (sUriMatcher.match(uri)) {
// If the incoming URI was for all of table3
case 1:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
// If the incoming URI was for a single row
case 2:
/*
* Because this URI was for a single row, the _ID value part is
* present. Get the last path segment from the URI; this is the _ID value.
* Then, append the value to the WHERE clause for the query
*/
selection = selection + "_ID = " + uri.getLastPathSegment();
break;
default:
...
// If the URI is not recognized, you should do some error handling here.
}
// call the code to actually do the query
}
複製程式碼
另一個類 ContentUris 會提供一些工具方法,用於處理內容 URI 的 id 部分。Uri 類和 Uri.Builder 類包括一些工具方法,用於解析現有 Uri 物件和構建新物件。
實現 ContentProvider 類
ContentProvider 例項通過處理來自其他應用的請求來管理對結構化資料集的訪問。 所有形式的訪問最終都會呼叫 ContentResolver,後者接著呼叫 ContentProvider 的具體方法來獲取訪問許可權。
必需方法
抽象類 ContentProvider 定義了六個抽象方法,您必須將這些方法作為自己具體子類的一部分加以實現。 所有這些方法(onCreate() 除外)都由一個嘗試訪問您的內容提供程式的客戶端應用呼叫:
query()
從您的提供程式檢索資料。使用引數選擇要查詢的表、要返回的行和列以及結果的排序順序。 將資料作為 Cursor 物件返回。
insert()
在您的提供程式中插入一個新行。使用引數選擇目標表並獲取要使用的列值。 返回新插入行的內容 URI。
update()
更新您提供程式中的現有行。使用引數選擇要更新的表和行,並獲取更新後的列值。 返回已更新的行數。
delete()
從您的提供程式中刪除行。使用引數選擇要刪除的表和行。 返回已刪除的行數。
getType()
返回內容 URI 對應的 MIME 型別。實現內容提供程式 MIME 型別部分對此方法做了更詳盡的描述。
onCreate()
初始化您的提供程式。Android 系統會在建立您的提供程式後立即呼叫此方法。 請注意,ContentResolver 物件嘗試訪問您的提供程式時,系統才會建立它。
複製程式碼
這些方法的簽名與同名的 ContentResolver 方法相同。
所有這些方法(onCreate() 除外)都可由多個執行緒同時呼叫,因此它們必須是執行緒安全方法。
避免在 onCreate() 中執行長時間操作。將初始化任務推遲到實際需要時進行。
儘管您必須實現這些方法,但您的程式碼只需返回要求的資料型別,無需執行任何其他操作。 例如,您可能想防止其他應用向某些表插入資料。 要實現此目的,您可以忽略 insert() 呼叫並返回 0。
實現 query() 方法
ContentProvider.query() 方法必須返回 Cursor 物件。如果失敗,則會引發 Exception。 如果您使用 SQLite 資料庫作為資料儲存,則只需返回由 SQLiteDatabase 類的其中一個 query() 方法返回的 Cursor。 如果查詢不匹配任何行,您應該返回一個 Cursor 例項(其 getCount() 方法返回 0)。只有當查詢過程中出現內部錯誤時,您才應該返回 null。
如果您不使用 SQLite 資料庫作為資料儲存,請使用 Cursor 的其中一個具體子類。 例如,在 MatrixCursor 類實現的遊標中,每一行都是一個 Object 陣列。 對於此類,請使用 addRow() 來新增新行。
請記住,Android 系統必須能夠跨程式邊界傳播 Exception。 Android 可以為以下異常執行此操作,這些異常可能有助於處理查詢錯誤:
IllegalArgumentException(您可以選擇在提供程式收到無效的內容 URI 時引發此異常) NullPointerException
實現 insert() 方法
insert() 方法會使用 ContentValues 引數中的值向相應表中新增新行。 如果 ContentValues 引數中未包含列名稱,您可能想在您的提供程式程式碼或資料庫架構中提供其預設值。
此方法應該返回新行的內容 URI。要想構建此方法,請使用 withAppendedId() 向表的內容 URI 追加新行的 _ID(或其他主鍵)值。
實現 delete() 方法
delete() 方法不需要從您的資料儲存中實際刪除行。 如果您將同步介面卡與提供程式一起使用,應該考慮為已刪除的行新增“刪除”標誌,而不是將行整個移除。 同步介面卡可以檢查是否存在已刪除的行,並將它們從伺服器中移除,然後再將它們從提供程式中刪除。
實現 update() 方法
update() 方法採用 insert() 所使用的相同 ContentValues 引數,以及 delete() 和 ContentProvider.query() 所使用的相同 selection 和 selectionArgs 引數。
實現 onCreate() 方法
Android 系統會在啟動提供程式時呼叫 onCreate()。您只應在此方法中執行執行快速的初始化任務,並將資料庫建立和資料載入推遲到提供程式實際收到資料請求時進行。 如果您在 onCreate() 中執行長時間的任務,則會減慢提供程式的啟動速度, 進而減慢提供程式對其他應用的響應速度。
例如,如果您使用 SQLite 資料庫,可以在 ContentProvider.onCreate() 中建立一個新的 SQLiteOpenHelper 物件,然後在首次開啟資料庫時建立 SQL 表。 為簡化這一過程,在您首次呼叫 getWritableDatabase() 時,它會自動呼叫 SQLiteOpenHelper.onCreate() 方法。
以下兩個程式碼段展示了 ContentProvider.onCreate() 與 SQLiteOpenHelper.onCreate() 之間的互動。第一個程式碼段是 ContentProvider.onCreate() 的實現:
public class ExampleProvider extends ContentProvider
/*
* Defines a handle to the database helper object. The MainDatabaseHelper class is defined
* in a following snippet.
*/
private MainDatabaseHelper mOpenHelper;
// Defines the database name
private static final String DBNAME = "mydb";
// Holds the database object
private SQLiteDatabase db;
public boolean onCreate() {
/*
* Creates a new helper object. This method always returns quickly.
* Notice that the database itself isn't created or opened
* until SQLiteOpenHelper.getWritableDatabase is called
*/
mOpenHelper = new MainDatabaseHelper(
getContext(), // the application context
DBNAME, // the name of the database)
null, // uses the default SQLite cursor
1 // the version number
);
return true;
}
...
// Implements the provider's insert method
public Cursor insert(Uri uri, ContentValues values) {
// Insert code here to determine which table to open, handle error-checking, and so forth
...
/*
* Gets a writeable database. This will trigger its creation if it doesn't already exist.
*
*/
db = mOpenHelper.getWritableDatabase();
}
}
複製程式碼
下一個程式碼段是 SQLiteOpenHelper.onCreate() 的實現,其中包括一個幫助程式類:
...
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
"main " + // Table's name
"(" + // The columns in the table
" _ID INTEGER PRIMARY KEY, " +
" WORD TEXT"
" FREQUENCY INTEGER " +
" LOCALE TEXT )";
...
/**
* Helper class that actually creates and manages the provider's underlying data repository.
*/
protected static final class MainDatabaseHelper extends SQLiteOpenHelper {
/*
* Instantiates an open helper for the provider's SQLite data repository
* Do not do database creation and upgrade here.
*/
MainDatabaseHelper(Context context) {
super(context, DBNAME, null, 1);
}
/*
* Creates the data repository. This is called when the provider attempts to open the
* repository and SQLite reports that it doesn't exist.
*/
public void onCreate(SQLiteDatabase db) {
// Creates the main table
db.execSQL(SQL_CREATE_MAIN);
}
}
複製程式碼
實現內容提供程式 MIME 型別
ContentProvider 類具有兩個返回 MIME 型別的方法:
getType()
您必須為任何提供程式實現的必需方法之一。
getStreamTypes()
系統在您的提供程式提供檔案時要求實現的方法。
表的 MIME 型別
getType() 方法會返回一個 MIME 格式的 String,後者描述內容 URI 引數返回的資料型別。Uri 引數可以是模式,而不是特定 URI;在這種情況下,您應該返回與匹配該模式的內容 URI 關聯的資料型別。
對於文字、HTML 或 JPEG 等常見資料型別,getType() 應該為該資料返回標準 MIME 型別。
對於指向一個或多個表資料行的內容 URI,getType() 應該以 Android 供應商特有 MIME 格式返回 MIME 型別:
型別部分:vnd
子型別部分:
- 如果 URI 模式用於單個行:android.cursor.item/
- 如果 URI 模式用於多個行:android.cursor.dir/
提供程式特有部分:vnd.<name>.<type>
您提供 <name>
和 <type>
。 <name>
值應具有全域性唯一性,<type>
值應在對應的 URI 模式中具有唯一性。 適合選擇貴公司的名稱或您的應用 Android 軟體包名稱的某個部分作為 <name>
。 適合選擇 URI 關聯表的標識字串作為 <type>
。
例如,如果提供程式的授權是 com.example.app.provider,並且它公開了一個名為 table1 的表,則 table1 中多個行的 MIME 型別是:
vnd.android.cursor.dir/vnd.com.example.provider.table1
複製程式碼
對於 table1 的單個行,MIME 型別是:
vnd.android.cursor.item/vnd.com.example.provider.table1
複製程式碼
檔案的 MIME 型別
如果您的提供程式提供檔案,請實現 getStreamTypes()。 該方法會為您的提供程式可以為給定內容 URI 返回的檔案返回一個 MIME 型別 String 陣列。 您應該通過 MIME 型別過濾器引數過濾您提供的 MIME 型別,以便只返回客戶端想處理的那些 MIME 型別。
例如,假設提供程式以 .jpg、.png 和 .gif 格式檔案形式提供照片影象。 如果應用呼叫 ContentResolver.getStreamTypes() 時使用了過濾器字串 image/*(任何是“影象”的內容),則 ContentProvider.getStreamTypes() 方法應返回陣列:
{ "image/jpeg", "image/png", "image/gif"}
複製程式碼
如果應用只對 .jpg 檔案感興趣,則可以在呼叫 ContentResolver.getStreamTypes() 時使用過濾器字串 */jpeg,ContentProvider.getStreamTypes() 應返回:
{"image/jpeg"}
複製程式碼
如果您的提供程式未提供過濾器字串中請求的任何 MIME 型別,則 getStreamTypes() 應返回 null。
實現協定類
協定類是一種 public final 類,其中包含對 URI、列名稱、MIME 型別以及其他與提供程式有關的後設資料的常量定義。 該類可確保即使 URI、列名稱等資料的實際值發生變化,也可以正確訪問提供程式,從而在提供程式與其他應用之間建立協定。
實現內容提供程式許可權
預設情況下,儲存在裝置內部儲存上的資料檔案是您的應用和提供程式的私有資料檔案; 您建立的 SQLiteDatabase 資料庫是您的應用和提供程式的私有資料庫; 預設情況下,您儲存到外部儲存的資料檔案是公用並可全域性讀取的資料檔案。 您無法使用內容提供程式來限制對外部儲存內檔案的訪問,因為其他應用可以使用其他 API 呼叫來對它們執行讀取和寫入操作; 用於在您的裝置的內部儲存上開啟或建立檔案或 SQLite 資料庫的方法呼叫可能會為所有其他應用同時授予讀取和寫入訪問許可權。 如果您將內部檔案或資料庫用作提供程式的儲存區,並向其授予“可全域性讀取”或“可全域性寫入”訪問許可權,則您在清單檔案中為提供程式設定的許可權不會保護您的資料。 內部儲存中檔案和資料庫的預設訪問許可權是“私有”,對於提供程式的儲存區,您不應更改此許可權。 如果您想使用內容提供程式許可權來控制對資料的訪問,則應將資料儲存在內部檔案、SQLite 資料庫或“雲”中(例如,遠端伺服器上),而且您應該保持檔案和資料庫為您的應用所私有。
實現許可權
即使底層資料為私有資料,所有應用仍可從您的提供程式讀取資料或向其寫入資料,因為在預設情況下,您的提供程式未設定許可權。 要想改變這種情況,請使用屬性或 <provider>
元素的子元素在您的清單檔案中為您的提供程式設定許可權。 您可以設定適用於整個提供程式、特定表甚至特定記錄的許可權,或者設定同時適用於這三者的許可權。
您可以通過清單檔案中的一個或多個 <permission>
元素為您的提供程式定義許可權。要使許可權對您的提供程式具有唯一性,請為 android:name 屬性使用 Java 風格作用域。 例如,將讀取許可權命名為 com.example.app.provider.permission.READ_PROVIDER。
以下列表描述了提供程式許可權的作用域,從適用於整個提供程式的許可權開始,然後逐漸細化。 更細化的許可權優先於作用域較大的許可權:
統一讀寫提供程式級別許可權
一個同時控制對整個提供程式讀取和寫入訪問的許可權,通過 <provider>
元素的 android:permission 屬性指定。
單獨的讀取和寫入提供程式級別許可權
針對整個提供程式的讀取許可權和寫入許可權。您可以通過 <provider>
元素的 android:readPermission 屬性和 android:writePermission 屬性 指定它們。它們優先於 android:permission 所需的許可權。
路徑級別許可權
針對提供程式中內容 URI 的讀取、寫入或讀取/寫入許可權。您可以通過 <provider>
元素的 <path-permission>
子元素指定您想控制的每個 URI。 對於您指定的每個內容 URI,您都可以指定讀取/寫入許可權、讀取許可權或寫入許可權,或同時指定所有三種許可權。 讀取許可權和寫入許可權優先於讀取/寫入許可權。 此外,路徑級別許可權優先於提供程式級別許可權。
臨時許可權
一種許可權級別,即使應用不具備通常需要的許可權,該級別也能授予對應用的臨時訪問許可權。 臨時訪問功能可減少應用需要在其清單檔案中請求的許可權數量。 當您啟用臨時許可權時,只有持續訪問您的所有資料的應用才需要“永久性”提供程式訪問許可權。 假設您需要實現電子郵件提供程式和應用的許可權,如果您想允許外部影象檢視器應用顯示您的提供程式中的照片附件, 為了在不請求許可權的情況下為影象檢視器提供必要的訪問許可權,可以為照片的內容 URI 設定臨時許可權。 對您的電子郵件應用進行相應設計,使應用能夠在使用者想要顯示照片時向影象檢視器傳送一個 Intent,其中包含照片的內容 URI 以及許可權標誌。 影象檢視器可隨後查詢您的電子郵件提供程式以檢索照片,即使檢視器不具備對您提供程式的正常讀取許可權,也不受影響。
要想啟用臨時許可權,請設定 <provider>
元素的 android:grantUriPermissions 屬性,或者向您的 <provider>
元素新增一個或多個 <grant-uri-permission>
子元素。如果您使用了臨時許可權,則每當您從提供程式中移除對某個內容 URI 的支援,並且該內容 URI 關聯了臨時許可權時,都需要呼叫 Context.revokeUriPermission()。
該屬性的值決定可訪問的提供程式範圍。 如果該屬性設定為 true,則系統會向整個提供程式授予臨時許可權,該許可權將替代您的提供程式級別或路徑級別許可權所需的任何其他許可權。
如果此標誌設定為 false,則您必須向 <provider>
元素新增 <grant-uri-permission>
子元素。每個子元素都指定授予的臨時許可權所對應的一個或多個內容 URI。
要嚮應用授予臨時訪問許可權,Intent 必須包含 FLAG_GRANT_READ_URI_PERMISSION 和/或 FLAG_GRANT_WRITE_URI_PERMISSION 標誌。 它們通過 setFlags() 方法進行設定。
如果 android:grantUriPermissions 屬性不存在,則假設其為 false。
元素
與 Activity 和 Service 元件類似,必須使用 <provider>
元素在清單檔案中為其應用定義 ContentProvider 的子類。 Android 系統會從該元素獲取以下資訊:
授權 (android:authorities) 用於在系統內標識整個提供程式的符號名稱。
提供程式類名 ( android:name ) 實現 ContentProvider 的類。
許可權
指定其他應用訪問提供程式的資料所必須具備許可權的屬性: android:grantUriPermssions:臨時許可權標誌 android:permission:統一提供程式範圍讀取/寫入許可權 android:readPermission:提供程式範圍讀取許可權 android:writePermission:提供程式範圍寫入許可權
啟動和控制屬性
這些屬性決定 Android 系統如何以及何時啟動提供程式、提供程式的程式特性以及其他執行時設定: android:enabled:允許系統啟動提供程式的標誌。 android:exported:允許其他應用使用此提供程式的標誌。 android:initOrder:此提供程式相對於同一程式中其他提供程式的啟動順序。 android:multiProcess:允許系統在與呼叫客戶端相同的程式中啟動提供程式的標誌。 android:process:應在其中執行提供程式的程式的名稱。 android:syncable:指示提供程式的資料將與伺服器上的資料同步的標誌。
資訊屬性
提供程式的可選圖示和標籤:
android:icon:包含提供程式圖示的可繪製物件資源。 該圖示出現在Settings > Apps > All 中應用列表內的提供程式標籤旁;
android:label:描述提供程式和/或其資料的資訊標籤。 該標籤出現在Settings > Apps > All中的應用列表內。
Intent 和資料訪問
應用可以通過 Intent 間接訪問內容提供程式。 應用不會呼叫 ContentResolver 或 ContentProvider 的任何方法,它並不會直接提供,而是會傳送一個啟動某個 Activity 的 Intent,該 Activity 通常是提供程式自身應用的一部分。 目標 Activity 負責檢索和顯示其 UI 中的資料。 視 Intent 中的操作而定,目標 Activity 可能還會提示使用者對提供程式的資料進行修改。 Intent 可能還包含目標 Activity 在 UI 中顯示的“extra”資料;使用者隨後可以選擇更改此資料,然後使用它來修改提供程式中的資料。