内容提供器 - Content Providers

Content Providers

    Content providers是用来管理对结构化数据集进行访问的一组接口。这组接口对数据进行封装,并提供了用于定义数据安全的机制。Content providers是一个进程使用另外一个进程数据的标准接口。
    当要使用content provider访问数据时,咱们须要在应用程序的Context中使用ContentResolver对象做为客户端,同provider进行通讯。与provier对象通讯的ContentResolver对象是ContentProvider类的一个实例。provider对象接收从客户端发来的数据,执行请求的动做并返回结果。
    若是你不打算同其余应用程序共享数据,就不必实现provider。可是,若是但愿在本身的应用程序中搜索建议的功能,就须要实现本身的provider。一样的,若是但愿在本身的应用程序和其余的应用程序间拷贝粘贴复杂的数据或文件,也须要实现本身的provider。
    Android系统自己也经过content providers来管理数据,如音频,视频,图像,我的联系信息等。咱们能够在android.provider包的参考文档中看到这些providers列表。在必定条件下,这些providers可以访问任何Android应用程序。
    接下来,将就content providers的如下题目作详细说明:
php

Content Provider 基本介绍 - Content Provider Basics
当数据以表的方式组织时,如何经过content provider访问数据。
建立Content Provider - Creating a Content Provider
如何建立本身的content provider。
日历Provider - Calendar Provider
如何访问Android平台的Calendar Provider。

Content Provider 基本介绍 - Content Provider Basics

    Content provider管理对于中央数据库的访问。而provider是Android应用程序的一部分,一般每一个provider本身提供界面同数据交互。然而,content providers最主要的目的是为了让其余应用程序使用provider的客户端对象访问provider。因此,providers和provider客户端共同提供了一个一致的标准数据接口用来处理进程间通讯和安全的数据访问。
本节主要介绍如下几个方面的内容:
html

  • Content providers是如何工做的。
  • 如何使用API从content provider中接收数据。
  • 在content provider中如何使用API进行数据的插入,更新或删除。
  • Providers提供的其余API功能,以便更好的使用providers。

概述-Overview

    Content provider以相似于关系数据库中一个或多个表的方式给外部的应用程序提供数据。一行表明provider收集的一些类型数据的一个实例,一列中的每一行表明数据中某个类型的一个实例。
    例如,用户字典就是Android平台内置的providers中的一个,用来保存用户但愿保存的非标准词的拼写。表1展现了provider表中数据可能的存储方式:
    表1:用户字典表的例子
android

word app id frequency locale _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

    在表1中,每行表明了在标准字典中可能找不到的一个词的实例。每一列代表这个词的一些数据,例如它第一次出现时的语言环境。列标题是存储在provider中的列名。为了找到一行的语言环境,你就须要索引到locale这个列所指向的内容。对于此provider来讲,_ID列做为主键提供自动索引。
数据库

注:主键对于provider来讲不是必须的,即便有主键,provider也没必要必定要使用_ID做为主键的列名。可是,若是但愿将provider的数据绑定到ListView上,就要使用_ID做为一列的名字。这些会在显示查询结果的小结中详细介绍。数组

访问 provider - Accessing a provider

    应用程序使用ContentResolver客户端对象来访问content provider的数据。ContentResolver对象与ContentProvider的一个具体子类的实例拥有相同名字的接口。ContentResolver的方法提供了基本的‘CRUD’(建立,检索,更新和删除)数据存储的功能。
    客户端应用程序进程中的ContentResolver对象和咱们本身的应用程序中的ContentProvider对象会自动处理进程间通讯。ContentProvider也会做为其存储的数据和数据以表的形式展示之间的抽象层。
安全

注:为访问provider,应用程序一般须要在manifest文件中添加特定的权限。这些将在Content Provider 权限小结中详细介绍。app

    举例来讲,为了从用户字典的Provider中得到单词和它们出现的语言环境列表,就须要调用ContentResolver.query()方法。query()方法会调用在用户字典的Provider中定义的ContentProvider.query()方法。下面这行代码展现了ContentResolver.query()是如何调用的:异步

// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // 词表的内容URI
    mProjection,                        // 每行中返回数据的列的名称,
                                        // null表示返回全部列的数据。
    mSelectionClause                    // 过滤条件
    mSelectionArgs,                     // 过滤条件的参数
    mSortOrder);                        // 返回行的排序方式

    表2介绍了函数query(Uri,projection,selection,selectionArgs,sortOrder)的参数同SQL SELECT语句之间的关系:
    表2:Query()函数与SQL的查询语句间的对应关系
ide

query() argument SELECT keyword/parameter Notes
Uri FROM table_name provider中的Uri至关于表名。
projection col,col,col,... projection 是一个列名的数组,规定了查询结果中每行应该包含哪些列。
selection WHERE col = value selection 指定了符合条件的行。
selectionArgs (No exact equivalent. Selection arguments replace ? placeholders in the selection clause.)  
sortOrder ORDER BY col,col,... sortOrder 指定了在返回结果Cursor中行排序的规则。

内容 URI - Content URIs

    内容URI是provider中用来惟一标识数据的URI。内容URIs包括整个provider的符号名称(它的权威)和指向表的名字(路径)。当调用客户端的方法来访问provider中的一个表时,这个表的内容URI会做为这个方法一个参数。
    在前面的代码中,常量CONTENT_URI包含用户词典中的词表的内容URI。ContentResolver对象解析出URI的权威,并将其与系统已知provider的权威表比较,从而获取对应的provider。而后,ContentResolver就能够将查询参数发送给正确的provider。
    ContentProvider根据内容URI中的路径部分选择须要访问的表。Provider一般为每一个表都公开一个路径。
    在前面的程序中,“词”表的完整URI以下所示:函数

content://user_dictionary/words

    这里的user_dictionary是provider的权威,words是表的路径。content://(大纲)是必定要有的,做为内容URI的惟一标识。
    许多provider容许经过在URI结尾追加一个ID值来访问表中的单独一行。例如,从用户词典中接收_ID为4的那行数据,就要使用以下的内容URI:

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

    当咱们但愿查询多行数据,而后更新或删除其中一行的数据时,就会常常用到id值。

注:UriUri.Builder类包含了从字符串构建Uri对象的一些方法。ContentUris包含了将id值追加到URI结尾的方法。前面的程序段使用withAppendedId()方法将id追加到用户词典的内容URI后面。

从Provider中检索数据 - Retrieving Data from the Provider

    本节介绍了如何从provider中检索数据,使用用户词典的Provider来举例说明。

为了方便起见,本节中的代码会在“UI线程”中调用ContentResolver.query()函数。可是,在实际代码中,应该将查询操做放到一个单独的线程中异步执行。一种方法就是使用CursorLoader类,这个类的详细说明能够在Loaders手册中查到。此外,例子代码仅仅是一部分,它们并无显示完整的应用程序。

    为了从provider中检索数据,请遵循如下的基本步骤:

  1. 得到provider的读访问权限。
  2. 定义发送给provider的查询代码。

得到读访问权限-Requesting read access permission

    为了从provider中查询数据,应用程序必须有此provider的读访问权限。该权限只能在应用程序的manifest文件中指定,而不能在运行时得到,使用<uses-permission>标签,并指明该provider定义的此权限的确切名称。当在应用程序的manifest文件中指定了该标签,其实是要求为您的应用程序赋予此权限。当用户安装你的应用时,会隐式的赋予此权限。
    要知道你使用的provider的读访问权限和其余权限的确切名字,请参考provider的文档。
    在 Content Provider Permissions中详细说明了访问provider的各个权限的做用。
    用户词表的Provider在它的manifest文件中定义了android.permission.READ_USER_DICTIONARY权限,因此若是一个应用程序但愿从provider中读取信息,就必须得到此权限。

构建查询程序 - Constructing the query

    从provider中检索数据的下一步就是构建查询程序。程序的第一部分定义了访问用户词典provider所须要的一些变量:

// projection 定义了返回的结果中需包含的列。
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
};
 
// 定义一个字符串用来包含selection的条件。
String mSelectionClause = null;
 
// 初始化数组包含selection的参数。
String[] mSelectionArgs = {""};

    接下来的程序以用户词典的Provider为例,显示如何使用ContentResolver.query()。Provider客户端查询相似于SQL查询,它包含一组返回的列值,一组查询条件和排序规则。
    查询返回的列的集合被称做一个projection(即变量mProjection)。
    检索指定行的条件表达式被分红selection语句和selection参数。selection语句是由逻辑和布尔表达式,列名和值(变量mSelection)组成的。若是指定的是替换参数而不是值,查询方法会从selection参数数组(变量mSelectionArgs)中检索对应的值。
    下一段程序中,若是用户没有输入任何单词,selection语句被设置成null,查询会返回provider中全部的单词。若是用户输入一个单词,查询语句被设置成UserDictionary.Words.Word + " = ?",同时selection参数数组中的第一个元素被设置成用户输入的单词。

/*
 * 这里定义了只有一个元素的字符串数组用来包含 selection 的参数。
 */
String[] mSelectionArgs = {""};
 
// 从UI得到单词
mSearchString = mSearchWord.getText().toString();
 
// 这里记着要添加错误检查。
 
// 若是输入的单词为空,返回全部词。
if (TextUtils.isEmpty(mSearchString)) {
    // 设置selection语句,若是为空返回全部单词
    mSelectionClause = null;
    mSelectionArgs[0] = "";
 
} else {
    // 构建selection语句来匹配用户输入的单词
    mSelectionClause = " = ?";
 
    // 将用户输入的单词放到selection的参数列表中
    mSelectionArgs[0] = mSearchString;
 
}
 
// 查询表,并返回一个Cursor对象
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // 单词表的URI
    mProjection,                       // 返回结果中每行包含的列
    mSelectionClause                   // 用户输入的单词或null
    mSelectionArgs,                    // 用户输入的字符串或null
    mSortOrder);                       // 返回行的排序规则
 
// 一些provider在查询发生错误后会返回null,其余的会抛出异常
if (null == mCursor) {
    /*
     * 这里须要添加你的错误处理程序,能够调用
     * android.util.Log.e()函数输出日志
     *
     */
// 若是Cursor为空,代表provider中没有匹配条件的结果
} else if (mCursor.getCount() < 1) {
 
    /*
     * 添加代码通知用户查询未成功。这些不必定是错误。能够供用户选择是插入
     * 新的一行仍是从新输入检索词。
     */
 
} else {
    // 插入对查询结果的操做
 
}
 
    此查询相似于下面的SQL语句:
 
SELECT _ID, word, frequency, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

    在这条SQL语句中用实际的列名替换了contract类的常量。

防止恶意输入 - Protecting against malicious input

    若是content provider管理的数据保存在SQL数据库中,在数据库中包含了外部不信任的数据,可能会引发SQL注入的风险。
    考虑下面的selection语句:

//  构建一个selection语句直接将用户输入链接到列名后面
String mSelectionClause =  "var = " + mUserInput;

    若是咱们这样写程序,将容许用户把恶意的SQL语句链接到你的SQL语句中。例如,用户能够在变量mUserInput中输入“nothing; DROP TABLE *;”,会致使selection语句变为var = nothing; DROP TABLE *;
。由于selection语句会做为SQL语句,因此这条语句可能会致使provider删除底层SQLite数据库中的全部表(除非provider创建了捕获SQL注入的机制)。
    为了不SQL注入,在selection语句中使用做为替代参数,并使用一个单独的selection参数数组。当使用参数的时候,用户的输入被直接添加到查询中,而不是被解释为SQL语句的一部分。由于它不是做为SQL处理,用户输入不能注入恶意的SQL。使用selection语句参数而不是直接将用户的输入链接起来的方式以下:

// 构建一个用参数替代的selection语句
String mSelectionClause =  "var = ?";

    用以下方式创建selection的参数数组:

// 定义一个包含selection的数组
String[] selectionArgs = {""};

    以以下方式将一个值放入selection的参数数组中:

// 将用户的输入添加到selection的参数数组中
selectionArgs[0] = mUserInput;

    一个使用做为替代参数并使用了selection参数数组的selection语句是使用selection的首要方式,即便provider并非基于SQL数据库。

显示查询结果 - Displaying query results

    客户端的ContentResolver.query()方法会返回一个Cursor对象,包含了知足查询条件的行及由查询的projection指定的列。一个Crusor对象为它所包含的行和列提供了随机读取的访问。使用Cursor的方法,能够遍历查询结果中的全部行,肯定每列的数据类型,获取一列的数据,及检验结果的其余属性。当provider的数据改变时,有些Crusor实现了自动更新的功能,或者当Crusor改变时,会触发一个监听对象的方法,或者二者兼而有之。

注:provider可能会基于查询对象自己而限制对一些列的访问。例如,联系人的Provider会限制对同步适配器的一些列的访问,因此这些列将不会返回给activity或service。

    若是没有行能匹配selection条件,provider会返回一个Cursor.getCount()数量为0的Cursor对象(一个空的cursor)。
    若是发生内部错误,查询的结果取决于具体的provider。有的会返回null,有的会抛出异常
    由于Cursor是全部行的列表,因此显示Cursor内容的一个好的方式是使用SimpleCursorAdapter将其链接到ListView上。
    下面的代码延续了前面的程序。建立了一个SimpleCursorAdapter对象,其中包含了查询结果的Cursor,并将此对象设置为一个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);

注:若要用Cursor来备份ListView,就须要此cursor包含一个名为_ID的列。正由于这样,前面对单词表的查询会收到_ID这一列,即便ListView并无显示该列。此限制条件也说明了为何大多数providers会在它们的每一个表中包含一个_ID列。

从查询结果中得到数据 - Getting data from query results

    咱们还能够将查询结果用于别的任务,而不是仅仅将其显示出来。例如,你能够从用户词典中检索出拼写,而后在其余的provider中查找它们。为了实现这点,你须要在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.
}

    Cursor实现了包含各类“get”的方法用来从对象中得到不一样类型的数据。例如,上面的程序中使用getString()方法。也可使用getType()方法得到一个值,显示该列的数据类型。

Content Provider 权限-Content Provider Permissions

    提供provider的应用程序能够指定访问权限,以便其余应用可以访问provider提供的数据。此权限能够保证用户可以知道应用程序中的那些数据能够访问。基于provider提供的说明,其余的应用程序能够根据自身的需求来请求权限去访问provider。最后,用户在安装此应用时会看到该应用请求得到的权限。
    若是包含provider的应用没有指定任何权限,其余的应用程序是没法访问该provider的数据的。可是包含provider应用的其余组件拥有对该provider的彻底读写权限,不管是否指定了权限。
    正如上面所说,用户字典的Provider要android.permission.READ_USER_DICTIONARY权限来控制对其数据的访问。此provider还提供了android.permission.WRITE_USER_DICTIONARY权限控制对数据的插入,更新和删除。
    为得到访问provider的权限,应用程序在manifest文件中须要使用<uses-permission>标签。当Android Package Manager 安装应用时,用户必须批准应用程序的全部权限请求。若是用户容许了,Package Manager会继续安装流程;若是用户不容许,Package Manager会终止安装。
    下面的<uses-permission>标签请求获取用户词典Provider的读权限:

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

    在安全性和权限中会详细介绍provider访问权限的影响。

插入,更新和删除数据 - Inserting, Updating, and Deleting Data

    同从provider获取数据的方式同样,咱们也要使用在provider的客户端和ContentProvider之间的交互来修改数据。调用ContentResolver的方法并传入参数,最终会调用ContentProvider的对应方法。provider和provider客户端会自动处理安全性和进程间通讯。