ContentProvider

内容提供程序管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。

如果您想要访问内容提供程序中的数据,可以将应用的 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,让您能够访问用户的所有联系人及其信息。 要执行此操作,您需要使用以下进程:

  1. 您的应用会使用方法 startActivityForResult() 发送包含操作 ACTION_PICK 和“联系人”MIME 类型 CONTENT_ITEM_TYPE 的 Intent 对象。
  2. 由于此 Intent 与“联系人”应用的“选择” Activity 的 Intent 过滤器相匹配,因此 Activity 会显示在前台。
  3. 在选择 Activity 中,用户选择要更新的联系人。 发生此情况时,选择 Activity 会调用 setResult(resultcode, intent) 以设置用于返回至应用的 Intent。Intent 包含用户选择的联系人的内容 URI,以及“extra”标志 FLAG_GRANT_READ_URI_PERMISSION。这些标志会为您的应用授予读取内容 URI 所指向联系人数据的 URI 权限。 然后,选择 Activity 会调用 finish(), 将控制权交还给您的应用。
  4. 您的 Activity 会返回至前台,系统会调用您的 Activity 的 onActivityResult() 方法。此方法会收到“联系人”应用中选择 Activity 所创建的结果 Intent。
  5. 通过来自结果 Intent 的内容 URI,您可以读取来自联系人提供程序的联系人数据,即使您未在清单文件中请求对该提供程序的永久读取访问权限。 您可以获取联系人的生日信息或其电子邮件地址,然后发送电子祝福。
使用其他应用

允许用户修改您无权访问的数据的简单方法是激活具有权限的应用,让用户在其中执行工作。

例如,日历应用接受 ACTION_INSERT Intent 对象,这让您可以激活 应用的插入 UI。您可以在此 Intent(应用将使用该 Intent 来预先填充 UI)中传递“extra”数据。 由于定期事件具有复杂的语法,因此将事件插入日历提供程序的首选方式是激活具有 ACTION_INSERT 的日历应用,然后让用户在其中插入事件。

创建内容提供程序

  1. 为您的数据设计原始存储。内容提供程序以两种方式提供数据:

文件数据

通常存储在文件中的数据,如照片、音频或视频。 将文件存储在您的应用的私有空间内。 您的提供程序可以应其他应用发出的文件请求提供文件句柄。

“结构化”数据

通常存储在数据库、数组或类似结构中的数据。 以兼容行列表的形式存储数据。行表示实体,如人员或库存项目。 列表示实体的某项数据,如人员的姓名或商品的价格。 此类数据通常存储在 SQLite 数据库中,但您可以使用任何类型的持久存储。

  1. 定义 ContentProvider 类及其所需方法的具体实现。 此类是您的数据与 Android 系统其余部分之间的接口。
  2. 定义提供程序的授权字符串、其内容 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”数据;用户随后可以选择更改此数据,然后使用它来修改提供程序中的数据。

转载于:https://juejin.im/post/5cb330595188251ace1fefe0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值