Android高级开发第五讲--API之Content Providers

博客出自:http://blog.csdn.net/liuxian13183,转载注明出处! All Rights Reserved ! 

Android的四大数据存储方式:文件,Sqlite,SharedPreference,ContentProvider。

文件可以是txt,也可以是xml,或者其他;可以保存在asset里,这样只有本项目可以访问,保存在内存卡或者SD卡中,其他项目也可以访问的到;

Sqlite主要用来规范化字段存储,本质是个文件管理系统;可以设置权限world_writeable,让所有项目都可以访问,也可以不设,默认只有本项目可以使用。

SharedPreference是个轻量级的存储器,只有本项目可以访问其中的数据。

ContentProvider使用系统常量主要用来对数据进行操作,如短信,图片,音频,联系人等等,也可以自定义URI,为其他项目通过authority进行使用。

以下是ContentProvider的API翻译内容。

content provider 提供一套结构化数据访问。提供封装数据和数据安全机制。content provider是一个进程与另一个进程进行数据连接的确标准接口。
当你要访问content provider里的数据,你需要使用应用里的上下文对象作为访问者来与provider进行连接,它们之间是通过一个ContentProvider的实行例化对象进行通信的。这个provider对象接收client数据请求,执行操作,返回结果。
如果你不想与其他应用共享数据,你就不用开发自己的provider。但是,你需要在自己的应用里提供一套搜索建议机制。如果你想从你的应用复制或粘贴复杂的数据到其他应用,那你需要一个自己的provider。
android自定义了一些content provider来管理如音频、视频、图像、个人信息。可以通过android.provider.package来查找相关文档。有了这些约束,android应用都可以任意访问provider。
下列主题描述更详细的content provider信息。
Content Provider Basics
How to access data in a content provider when the data is organized in tables.
Creating a Content Provider
How to create your own content provider.
Calendar Provider
How to access the Calendar Provider that is part of the Android platform.
Contacts Provider
How to access the Contacts Provider that is part of the Android platform.

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

如果您想要访问内容提供程序中的数据,可以将应用的 Context 中的 ContentResolver 对象用作客户端来与提供程序通信。 ContentResolver 对象会与提供程序对象(即实现 ContentProvider 的类实例)通信。 提供程序对象从客户端接收数据请求,执行请求的操作并返回结果。

如果您不打算与其他应用共享数据,则无需开发自己的提供程序。 不过,您需要通过自己的提供程序在您自己的应用中提供自定义搜索建议。 如果您想将复杂的数据或文件从您的应用复制并粘贴到其他应用中,也需要创建您自己的提供程序。

Android 本身包括的内容提供程序可管理音频、视频、图像和个人联系信息等数据。 android.provider 软件包参考文档中列出了部分提供程序。 任何 Android 应用都可以访问这些提供程序,但会受到某些限制。

以下主题对内容提供程序做了更详尽的描述:

内容提供程序基础知识

如何访问内容提供程序中以表形式组织的数据。

创建内容提供程序

如何创建您自己的内容提供程序。

日历提供程序

如何访问作为 Android 平台一部分的日历提供程序。

联系人提供程序

如何访问作为 Android 平台一部分的联系人提供程序。

内容提供程序基础知识

内容提供程序管理对中央数据存储区的访问。提供程序是 Android 应用的一部分,通常提供自己的 UI 来使用数据。 但是,内容提供程序主要旨在供其他应用使用,这些应用使用提供程序客户端对象来访问提供程序。 提供程序与提供程序客户端共同提供一致的标准数据界面,该界面还可处理跨进程通信并保护数据访问的安全性。

本主题介绍了以下基础知识:

  • 内容提供程序的工作方式。
  • 用于从内容提供程序检索数据的 API。
  • 用于在内容提供程序中插入、更新或删除数据的 API。
  • 其他有助于使用提供程序的 API 功能。

概览


内容提供程序以一个或多个表(与在关系型数据库中找到的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。

例如,Android 平台的内置提供程序之一是用户字典,它会存储用户想要保存的非标准字词的拼写。 表 1 描述了数据在此提供程序表中的显示情况:

表 1. 用户字典示例表格。

字词应用 id频率语言区域_ID
mapreduceuser1100en_US1
precompileruser14200fr_FR2
appletuser2225fr_CA3
constuser1255pt_BR4
intuser5100en_UK5

在表 1 中,每行表示可能无法在标准词典中找到的字词实例。 每列表示该字词的某些数据,如该字词首次出现时的语言区域。 列标题是存储在提供程序中的列名称。 要引用行的语言区域,需要引用其 locale 列。对于此提供程序,_ID 列充当由提供程序自动维护的“主键”列。

注:提供程序无需具有主键,也无需将 _ID 用作其主键的列名称(如果存在主键)。 但是,如果您要将来自提供程序的数据与 ListView 绑定,则其中一个列名称必须是 _ID。 显示查询结果部分详细说明了此要求。

访问提供程序

应用从具有 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
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

表 2 显示了 query(Uri,projection,selection,selectionArgs,sortOrder) 的参数如何匹配 SQL SELECT 语句:

表 2. Query() 与 SQL 查询对比。

query() 参数SELECT 关键字/参数说明
UriFROM table_nameUri 映射至提供程序中名为 table_name 的表。
projectioncol,col,col,...projection 是应该为检索到的每个行包含的列的数组。
selectionWHERE col = valueselection 会指定选择行的条件。
selectionArgs(没有完全等效项。选择参数会替换选择子句中 ? 占位符。)
sortOrderORDER BY col,col,...sortOrder 指定行在返回的 Cursor 中的显示顺序。

内容 URI

内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个提供程序的符号名称(其授权)和一个指向表的名称(路径)。 当您调用客户端方法来访问提供程序中的表时,该表的内容 URI 将是其参数之一。

在前面的代码行中,常量 CONTENT_URI 包含用户字典的“字词”表的内容 URI。 ContentResolver 对象会分析出 URI 的授权,并通过将该授权与已知提供程序的系统表进行比较,来“解析”提供程序。 然后, ContentResolver 可以将查询参数分派给正确的提供程序。

ContentProvider 使用内容 URI 的路径部分来选择要访问的表。 提供程序通常会为其公开的每个表显示一条路径

在前面的代码行中,“字词”表的完整 URI 是:

content://user_dictionary/words://user_dictionary/words

其中,user_dictionary 字符串是提供程序的授权,words 字符串是表的路径。 字符串 content://架构)始终显示,并将此标识为内容 URI。

许多提供程序都允许您通过将 ID 值追加到 URI 末尾来访问表中的单个行。 例如,要从用户字典中检索 _ID 为 4 的行,则可使用此内容 URI:

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

在检索到一组行后想要更新或删除其中某一行时通常会用到 ID 值。

注:Uri 和 Uri.Builder 类包含根据字符串构建格式规范的 URI 对象的便利方法。 ContentUris 包含一些可以将 ID 值轻松追加到 URI 后的方法。 前面的代码段就是使用 withAppendedId() 将 ID 追加到 UserDictionary 内容 URI 后。

从提供程序检索数据


本节将以用户字典提供程序为例,介绍如何从提供程序中检索数据。

为了明确进行说明,本节中的代码段将在“UI 线程”上调用 ContentResolver.query()。但在实际代码中,您应该在单独线程上异步执行查询。 执行此操作的方式之一是使用 CursorLoader 类,加载器指南中对此有更为详细的介绍。 此外,前述代码行只是片段;它们不会显示整个应用。

要从提供程序中检索数据,请按照以下基本步骤执行操作:

  1. 请求对提供程序的读取访问权限。
  2. 定义将查询发送至提供程序的代码。

请求读取访问权限

要从提供程序检索数据,您的应需要具备对提供程序的“读取访问”权限。 您无法在运行时请求此权限;相反,您需要使用<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 = {""};
// 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

}
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;, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

在此 SQL 语句中,会使用实际的列名称而非协定类常量。

防止恶意输入

如果内容提供程序管理的数据位于 SQL 数据库中,将不受信任的外部数据包括在原始 SQL 语句中可能会导致 SQL 注入。

考虑此选择子句:

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

如果您执行此操作,则会允许用户将恶意 SQL 串连到 SQL 语句上。 例如,用户可以为 mUserInput 输入“nothing; DROP TABLE *;”,这会生成选择子句 var = nothing; DROP TABLE *;。 由于选择子句是作为 SQL 语句处理,因此这可能会导致提供程序擦除基础 SQLite 数据库中的所有表(除非提供程序设置为可捕获 SQL 注入尝试)。

要避免此问题,可使用一个用于将 ? 作为可替换参数的选择子句以及一个单独的选择参数数组。 执行此操作时,用户输入直接受查询约束,而不解释为 SQL 语句的一部分。 由于用户输入未作为 SQL 处理,因此无法注入恶意 SQL。请使用此选择子句,而不要使用串连来包括用户输入:

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

按如下所示设置选择参数数组:

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

按如下所示将值置于选择参数数组中:

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

一个用于将 ? 用作可替换参数的选择子句和一个选择参数数组是指定选择的首选方式,即使提供程序并未基于 SQL 数据库。

显示查询结果

ContentResolver.query() 客户端方法始终会返回符合以下条件的 Cursor:包含查询的投影为匹配查询选择条件的行指定的列。 Cursor 对象为其包含的行和列提供随机读取访问权限。 通过使用 Cursor 方法,您可以循环访问结果中的行、确定每个列的数据类型、从列中获取数据,并检查结果的其他属性。 某些 Cursor 实现会在提供程序的数据发生更改时自动更新对象和/或在 Cursor 更改时触发观察程序对象中的方法。

注:提供程序可能会根据发出查询的对象的性质来限制对列的访问。 例如,联系人提供程序会限定只有同步适配器才能访问某些列,因此不会将它们返回至 Activity 或服务。

如果没有与选择条件匹配的行,则提供程序会返回 Cursor.getCount() 为 0(空游标)的 Cursor 对象。

如果出现内部错误,查询结果将取决于具体的提供程序。它可能会选择返回 null,或引发 Exception

由于 Cursor 是行“列表”,因此显示 Cursor 内容的良好方式是通过 SimpleCursorAdapter 将其与 ListView 关联。

以下代码段将延续上一代码段的代码。它会创建一个包含由查询检索到的 Cursor 的 SimpleCursorAdapter 对象,并将此对象设置为 ListView 的适配器:

// 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);
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);

注:要通过 Cursor 支持 ListView,游标必需包含名为 _ID 的列。 正因如此,前文显示的查询会为“字词”表检索 _ID 列,即使 ListView 未显示该列。 此限制也解释了为什么大多数提供程序的每个表都具有 _ID 列。

从查询结果中获取数据

您可以将查询结果用于其他任务,而不是仅显示它们。例如,您可以从用户字典中检索拼写,然后在其他提供程序中查找它们。 要执行此操作,您需要在 Cursor 中循环访问行:


// 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.
}// 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"><uses-permission android:name="android.permission.READ_USER_DICTIONARY">

安全与权限指南中详细介绍了权限对提供程序访问的影响。

插入、更新和删除数据


与从提供程序检索数据的方式相同,也可以通过提供程序客户端和提供程序 ContentProvider 之间的交互来修改数据。 您通过传递到 ContentProvider的对应方法的参数来调用 ContentResolver 方法。 提供程序和提供程序客户端会自动处理安全性和跨进程通信。

插入数据

要将数据插入提供程序,可调用 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
);
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>://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
);
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
);

您还应该在调用 ContentResolver.update() 时检查用户输入。如需了解有关此内容的更多详情,请阅读防止恶意输入部分。

删除数据

删除行与检索行数据类似:为要删除的行指定选择条件,客户端方法会返回已删除的行数。 以下代码段会删除应用 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
);// 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
);

您还应该在调用 ContentResolver.delete() 时检查用户输入。如需了解有关此内容的更多详情,请阅读防止恶意输入部分。

提供程序数据类型


内容提供程序可以提供多种不同的数据类型。用户字典提供程序仅提供文本,但提供程序也能提供以下格式:

  • 整型
  • 长整型(长)
  • 浮点型
  • 长浮点型(双倍)

提供程序经常使用的另一种数据类型是作为 64KB 字节的数组实施的二进制大型对象 (BLOB)。 您可以通过查看 Cursor 类“获取”方法看到可用数据类型。

提供程序文档中通常都列出了其每个列的数据类型。 用户字典提供程序的数据类型列在其协定类 UserDictionary.Words 参考文档中(协定类部分对协定类进行了介绍)。 您也可以通过调用 Cursor.getType() 来确定数据类型。

提供程序还会维护其定义的每个内容 URI 的 MIME(多用途互联网邮件扩展)数据类型信息。您可以使用 MIME 类型信息查明应用是否可以处理提供程序提供的数据,或根据 MIME 类型选择处理类型。 在使用包含复杂数据结构或文件的提供程序时,通常需要 MIME 类型。 例如,联系人提供程序中的 ContactsContract.Data 表会使用 MIME 类型来标记每行中存储的联系人数据类型。 要获取与内容 URI 对应的 MIME 类型,请调用ContentResolver.getType()

MIME 类型引用部分介绍了标准和自定义 MIME 类型的语法。

提供程序访问的替代形式


提供程序访问的三种替代形式在应用开发过程中十分重要:

下文将介绍通过 Intent 进行的批量访问和修改。

批量访问

批量访问提供程序适用于插入大量行,或通过同一方法调用在多个表中插入行,或者通常用于跨进程界限将一组操作作为事务处理(原子操作)执行。

要在“批量模式”下访问提供程序, 您可以创建 ContentProviderOperation 对象数组,然后使用 ContentResolver.applyBatch() 将其分派给内容提供程序。您需将内容提供程序的授权传递给此方法,而不是特定内容 URI。 这样可使数组中的每个 ContentProviderOperation 对象都能适用于其他表。 调用 ContentResolver.applyBatch() 会返回结果数组。

ContactsContract.RawContacts 协定类 的说明包括展示批量注入的代码段。 联系人管理器示例应用包含在其 ContactAdder.java 源文件中进行批量访问的示例。

使用帮助程序应用显示数据

如果您的应用具有访问权限,您可能仍想使用 Intent 对象在其他应用中显示数据。 例如,日历应用接受ACTION_VIEW Intent 对象,用于显示特定的日期或事件。 这样,您可以在不创建自己的 UI 的情况下显示日历信息。 如需了解有关此功能的更多信息,请参见日历提供程序指南。

您向其发送 Intent 对象的应用不必是与提供程序关联的应用。 例如,您可以从联系人提供程序中检索联系人,然后将包含联系人图像的内容 URI 的 ACTION_VIEW Intent 发送至图像查看器。

通过 Intent 访问数据

Intent 可以提供对内容提供程序的间接访问。即使您的应用不具备访问权限,您也可以通过以下方式允许用户访问提供程序中的数据:从具有权限的应用中获取回结果 Intent,或者通过激活具有权限的应用,然后让用户在其中工作。

通过临时权限获取访问权限

即使您没有适当的访问权限,也可以通过以下方式访问内容提供程序中的数据:将 Intent 发送至具有权限的应用,然后接收回包含“URI”权限的结果 Intent。 这些是特定内容 URI 的权限,将持续至接收该权限的 Activity 结束。 具有永久权限的应用将通过在结果 Intent 中设置标志来授予临时权限:

注:这些标志不会为其授权包含在内容 URI 中的提供程序提供常规的读取或写入访问权限。 访问权限仅适用于 URI 本身。

提供程序使用 <provider> 元素的 android:grantUriPermission 属性以及 <provider> 元素的 <grant-uri-permission> 子元素在其清单文件中定义内容 URI 的 URI 权限。安全与权限指南中“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 的日历应用,然后让用户在其中插入事件。

协定类


协定类定义帮助应用使用内容 URI、列名称、 Intent 操作以及内容提供程序的其他功能的常量。 协定类未自动包含在提供程序中;提供程序的开发者需要定义它们,然后使其可用于其他开发者。 Android 平台中包含的许多提供程序都在软件包 android.provider 中具有对应的协定类。

例如,用户字典提供程序具有包含内容 URI 和列名称常量的协定类 UserDictionary。 “字词”表的内容 URI 在常量 UserDictionary.Words.CONTENT_URI中定义。 UserDictionary.Words 类也包含列名称常量,本指南的示例代码段中就使用了该常量。 例如,查询投影可以定义为:

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

联系人提供程序的 ContactsContract 也是一个协定类。 此类的参考文档包括示例代码段。其子类之一 ContactsContract.Intents.Insert 是包含 Intent 和 Intent 数据的协定类。

MIME 类型引用


内容提供程序可以返回标准 MIME 媒体类型和/或自定义 MIME 类型字符串。

MIME 类型具有格式

type/subtype/subtype

例如,众所周知的 MIME 类型 text/html 具有 text 类型和 html 子类型。如果提供程序为 URI 返回此类型,则意味着使用该 URI 的查询会返回包含 HTML 标记的文本。

自定义 MIME 类型字符串(也称为“特定于供应商”的 MIME 类型)具有更加复杂的类型子类型值。 类型值始终为

vnd.android.cursor.dir.android.cursor.dir

(多行)或

vnd.android.cursor.item.android.cursor.item

(单行)。

子类型特定于提供程序。Android 内置提供程序通常具有简单的子类型。 例如,当“通讯录”应用为电话号码创建行时,它会在行中设置以下 MIME 类型:

vnd.android.cursor.item/phone_v2.android.cursor.item/phone_v2

请注意,子类型值只是 phone_v2

其他提供程序开发者可能会根据提供程序的授权和表名称创建自己的子类型模式。 例如,假设提供程序包含列车时刻表。 提供程序的授权是 com.example.trains,并包含表 Line1、Line2 和 Line3。在响应表 Line1 的内容 URI

content://com.example.trains/Line1://com.example.trains/Line1

时,提供程序会返回 MIME 类型

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

在响应表 Line2 中第 5 行的内容 URI

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

时,提供程序会返回 MIME 类型

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

大多数内容提供程序都会为其使用的 MIME 类型定义协定类常量。例如,联系人提供程序协定类 ContactsContract.RawContacts 会为单个原始联系人行的 MIME 类型定义常量 CONTENT_ITEM_TYPE

内容 URI 部分介绍了单个行的内容 URI。

创建内容提供程序

内容提供程序管理对中央数据存储区的访问。您将提供程序作为 Android 应用中的一个或多个类(连同清单文件中的元素)实现。 其中一个类会实现子类 ContentProvider,即您的提供程序与其他应用之间的接口。 尽管内容提供程序旨在向其他应用提供数据,但您的应用中必定有这样一些 Activity,它们允许用户查询和修改由提供程序管理的数据。

本主题的其余部分列出了开发内容提供程序的基本步骤和需要使用的 API。

着手开发前的准备工作


请在着手开发提供程序之前执行以下操作:

  1. 决定您是否需要内容提供程序。如果您想提供下列一项或多项功能,则需要开发内容提供程序:
    • 您想为其他应用提供复杂的数据或文件
    • 您想允许用户将复杂的数据从您的应用复制到其他应用中
    • 您想使用搜索框架提供自定义搜索建议

    如果完全是在您自己的应用中使用,则需要提供程序即可使用 SQLite 数据库。

  2. 如果您尚未完成此项操作,请阅读内容提供程序基础知识主题,了解有关提供程序的详情。

接下来,请按照以下步骤开发您的提供程序:

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

    文件数据

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

    “结构化”数据

    通常存储在数据库、数组或类似结构中的数据。 以兼容行列表的形式存储数据。行表示实体,如人员或库存项目。 列表示实体的某项数据,如人员的姓名或商品的价格。 此类数据通常存储在 SQLite 数据库中,但您可以使用任何类型的持久存储。 如需了解有关 Android 系统中提供的存储类型的更多信息,请参阅设计数据存储部分。

  2. 定义 ContentProvider 类及其所需方法的具体实现。 此类是您的数据与 Android 系统其余部分之间的接口。 如需了解有关此类的详细信息,请参阅实现 ContentProvider 类部分。
  3. 定义提供程序的授权字符串、其内容 URI 以及列名称。如果您想让提供程序的应用处理 Intent,则还要定义 Intent 操作、Extra 数据以及标志。 此外,还要定义想要访问您的数据的应用必须具备的权限。 您应该考虑在一个单独的协定类中将所有这些值定义为常量;以后您可以将此类公开给其他开发者。 如需了解有关内容 URI 的详细信息,请参阅设计内容 URI 部分。 如需了解有关 Intent 的详细信息,请参阅 Intent 和数据访问部分。
  4. 添加其他可选部分,如示例数据或可以在提供程序与云数据之间同步数据的 AbstractThreadedSyncAdapter 实现。

设计数据存储


内容提供程序是用于访问以结构化格式保存的数据的接口。在您创建该接口之前,必须决定如何存储数据。 您可以按自己的喜好以任何形式存储数据,然后根据需要设计读写数据的接口。

以下是 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 作为参数;您可以利用这一点确定要访问的表、行或文件。

内容提供程序基础知识主题中描述了内容 URI 的基础知识。

设计授权

提供程序通常具有单一授权,该授权充当其 Android 内部名称。为避免与其他提供程序发生冲突,您应该使用互联网网域所有权(反向)作为提供程序授权的基础。 由于此建议也适用于 Android 软件包名称,因此您可以将提供程序授权定义为包含该提供程序的软件包名称的扩展名。 例如,如果您的 Android 软件包名称为 com.example.<appname>,则应为提供程序提供 com.example.<appname>.provider 授权。

设计路径结构

开发者通常通过追加指向单个表的路径来根据权限创建内容 URI。 例如,如果您有两个表:table1 和 table2,则可以通过合并上一示例中的权限来生成 内容 URI com.example.<appname>.provider/table1 和 com.example.<appname>.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
    } 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() 中执行长时间操作。将初始化任务推迟到实际需要时进行。 实现 onCreate() 方法部分对此做了更详尽的描述;
  • 尽管您必须实现这些方法,但您的代码只需返回要求的数据类型,无需执行任何其他操作。 例如,您可能想防止其他应用向某些表插入数据。 要实现此目的,您可以忽略 insert() 调用并返回 0。

实现 query() 方法

ContentProvider.query() 方法必须返回 Cursor 对象。如果失败,则会引发 Exception。 如果您使用 SQLite 数据库作为数据存储,则只需返回由 SQLiteDatabase 类的其中一个 query() 方法返回的 Cursor。 如果查询不匹配任何行,您应该返回一个 Cursor 实例(其 getCount() 方法返回 0)。只有当查询过程中出现内部错误时,您才应该返回 null

如果您不使用 SQLite 数据库作为数据存储,请使用 Cursor 的其中一个具体子类。 例如,在 MatrixCursor 类实现的游标中,每一行都是一个 Object 数组。 对于此类,请使用 addRow() 来添加新行。

请记住,Android 系统必须能够跨进程边界传播 Exception。 Android 可以为以下异常执行此操作,这些异常可能有助于处理查询错误:

实现 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();
    }
} 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);
    }
}
// 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 类型。 IANA MIME Media Types 网站上提供了这些标准类型的完整列表。

对于指向一个或多个表数据行的内容 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.android.cursor.dir/vnd.com.example.provider.table1

对于 table1 的单个行,MIME 类型是:

vnd.android.cursor.item/vnd.com.example.provider.table1.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"} "image/jpeg", "image/png", "image/gif"}

如果应用只对 .jpg 文件感兴趣,则可以在调用 ContentResolver.getStreamTypes() 时使用过滤器字符串 *\/jpegContentProvider.getStreamTypes() 应返回:

{"image/jpeg"}"image/jpeg"}

如果您的提供程序未提供过滤器字符串中请求的任何 MIME 类型,则 getStreamTypes() 应返回 null

实现协定类


协定类是一种 public final 类,其中包含对 URI、列名称、MIME 类型以及其他与提供程序有关的元数据的常量定义。 该类可确保即使 URI、列名称等数据的实际值发生变化,也可以正确访问提供程序,从而在提供程序与其他应用之间建立协定。

协定类对开发者也有帮助,因为其常量通常采用助记名称,因此可以降低开发者为列名称或 URI 使用错误值的可能性。 由于它是一种类,因此可以包含 Javadoc 文档。 集成开发环境(如 Android Studio)可以根据协定类自动完成常量名称,并为常量显示 Javadoc。

开发者无法从您的应用访问协定类的类文件,但他们可以通过您提供的 .jar 文件将其静态编译到其应用内。

举例来说,ContactsContract 类及其嵌套类便属于协定类。

实现内容提供程序权限


安全与权限主题中详细描述了 Android 系统各个方面的权限和访问。 数据存储主题也描述了各类存储实行中的安全与权限。 其中的要点简述如下:

  • 默认情况下,存储在设备内部存储上的数据文件是您的应用和提供程序的私有数据文件;
  • 您创建的 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

<Provider> 元素


与 Activity 和 Service 组件类似,必须使用 <provider> 元素在清单文件中为其应用定义 ContentProvider 的子类。 Android 系统会从该元素获取以下信息:

授权 (android:authorities)

用于在系统内标识整个提供程序的符号名称。设计内容 URI 部分对此属性做了更详尽的描述。

提供程序类名 ( android:name )

实现 ContentProvider 的类。实现 ContentProvider 类中对此类做了更详尽的描述。

权限

指定其他应用访问提供程序的数据所必须具备权限的属性:

实现内容提供程序权限部分对权限及其对应属性做了更详尽的描述。

启动和控制属性

这些属性决定 Android 系统如何以及何时启动提供程序、提供程序的进程特性以及其他运行时设置:

开发指南中针对 <provider> 元素的主题提供了这些属性的完整资料。

信息属性

提供程序的可选图标和标签:
  • android:icon:包含提供程序图标的可绘制对象资源。 该图标出现在Settings > Apps > All 中应用列表内的提供程序标签旁;
  • android:label:描述提供程序和/或其数据的信息标签。 该标签出现在Settings > Apps > All中的应用列表内。

开发指南中针对 <provider> 元素的主题提供了这些属性的完整资料。

Intent 和数据访问


应用可以通过 Intent 间接访问内容提供程序。 应用不会调用 ContentResolver 或 ContentProvider 的任何方法,它并不会直接提供,而是会发送一个启动某个 Activity 的 Intent,该 Activity 通常是提供程序自身应用的一部分。 目标 Activity 负责检索和显示其 UI 中的数据。 视 Intent 中的操作而定,目标 Activity 可能还会提示用户对提供程序的数据进行修改。 Intent 可能还包含目标 Activity 在 UI 中显示的“extra”数据;用户随后可以选择更改此数据,然后使用它来修改提供程序中的数据。

您可能想使用 Intent 访问权限来帮助确保数据完整性。您的提供程序可能依赖于根据严格定义的业务逻辑插入、更新和删除数据。 如果是这种情况,则允许其他应用直接修改您的数据可能会导致无效的数据。 如果您想让开发者使用 Intent 访问权限,请务必为其提供详尽的参考资料。 向他们解释为什么使用自身应用 UI 的 Intent 访问比尝试通过代码修改数据更好。

处理想要修改您的提供程序数据的传入 Intent 与处理其他 Intent 没有区别。 您可以通过阅读 Intent 和 Intent 过滤器主题了解有关 Intent 用法的更多信息。

配置文件里,有两种不同的permission属性可以设置: android:readPermission 用于限制谁可以读取provider中的数据,而 android:writePermission 用于限制谁才可以向provider中写入数据。

来自谷歌:https://developer.android.com/guide/topics/providers/content-providers.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘兆贤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值