Android官方文档之Content Providers

Content Providers是Android中四大组件之一,用于管理应用程序访问结构化的数据。Content Providers可以压缩数据(They encapsulate the data),并保护访问数据的安全。Content Providers是应用程序访问跨进程数据的标准接口(standard interface that connects data in one process with code running in another process)。

本文将介绍Content Providers的相关知识,如需访问官方原文,您可以点击这个链接:《Content Providers》。

Content Providers

使用content provider访问数据,您应当在自己的应用程序的Context环境中使用ContentResolver对象,并将该对象作为client端,与content provider通信(实际是与ContentProvider的子类对象通信):content provider接收clients端发送的请求,并根据请求执行操作,最后将结果返回给client端。

如果您不打算把自己应用程序的数据分享给其他应用,那么无需创建自己的provider 。但是,如果需要在自己的程序中提供定制的搜索建议( to provide custom search suggestions in your own application)、或是从自己的应用向其他应用拷贝或粘贴大型而复杂的数据文件( copy and paste complex data or files from your application to other applications),那么您需要创建provider。

Android系统提供了各式各样的provider,如 音频(audio)、 视频(video)、 图片(images) 、联系人信息(personal contact information) 等。您可以在android.provider包中查看系统提供的provider。当然,访问部分provider需要系统提供访问权限。

Content Provider 基础(Content Provider Basics)

provider是Android应用程序的一部分。使用provider提供的数据可以进行UI展示。然而,provider最常用的情形是将本应用的数据提供给其他应用程序使用。使用provider的client端(实际上是content resolver)来访问provider。也就是说,provider一般与resolver配对使用,provider提供了访问数据的标准接口,可以方便resolver进行安全的跨进程通信(handles inter-process communication and secure data access)。

本节将讨论如下内容:

  • content providers 是如何工作的;

  • 从content provider中检索数据API的使用;

  • content provider中增删改查API的使用;

  • 其他content provider的API;

概述(Overview)

content provider可以向外部应用程序提供关系型数据库中的一张或多张表的信息。表中的每一行表示一种数据类型或某个人的信息详情。每一列代表一个字段。

比如说,Android系统个内置了一个provider,该provider提供了一张用户字典表(user dictionary),表中保存了用户经常输入的不标准的单词,如下所示:
Android官方文档之Content Providers_第1张图片

上表中,每一行表示一个单词的所有信息。每列表示单词的统计信息,如使用频率 等。其中_ID字段是表的主键,它被自动赋予。

!请注意:provider提供的表不一定必须提供主键,即便提供主键,名字也不一定必须是_ID。但是,若您需要将provider提供的表绑定到ListView上,那么该表必须包含主键,且名字必须是_ID(if you want to bind data from a provider to a ListView, one of the column names has to be _ID)。

访问provider(Accessing a provider)

在应用程序中使用ContentResolver对象,可以访问其他应用程序的provider,在该类中提供了与ContentProvider类中命名相同的方法( identically-named methods),包括增删改查( “CRUD” (create, retrieve, update, delete))。

ContentResolver所在进程与ContentProvider是不同的进程,所以这无形中实现了跨进程通信( inter-process communication)。而ContentProvider实际上还充当了抽象层,该层介于数据仓库和外部提供数据接口之间(acts as an abstraction layer between its repository of data and the external appearance of data as tables)。

!请注意:为了使ContentResolver能访问ContentProvider的数据,您可能需要在manifest文件中加入访问权限。

举例来说,若需要获得上表中word列和locale列的数据,需要在您的应用程序中调用ContentResolver.query()方法,接着该方法又会调用ContentProvider.query()方法,该方法由用户字典provider提供。下面演示了访问过程:

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

下表解释了query(Uri,projection,selection,selectionArgs,sortOrder)方法中的参数含义:

内容URIs(Content URIs)

内容URI指向了provider中提供的一组数据,内容URI由两部分组成,其中authority部分指定了需要访问的provider名,path部分指定了该provider中的某张表。

在上述代码中,CONTENT_URI指向了单词表,ContentResolverCONTENT_URI中解析出authority,并在系统中查找能与之匹配的provider。找到provider后,该provider利用URI的path部分寻找匹配的表。
所以,如上所述,words表应具有如下URI:

content://user_dictionary/words

其中user_dictionary为provider的authority,words为表的path,而前缀content://(scheme)则恒定不变,表示这是一个内容URI。

您也可以在URI后缀一个_ID来查询表中的某一行数据:

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

这表示希望查询单词表中_ID为4 的那一行数据。当您需要删除或更新某一行数据时,应当使用这种加入了_ID的URI。

!请注意:Uri类Uri.Builder类提供了便捷的方法,把String包装为格式规范的URI对象(well-formed URI objects from strings),而ContentUris类同样提供了便捷的方法,在URI后追加ID,上述代码的withAppendedId()就是为URI追加ID的方法。

从Provider中查询数据(Retrieving Data from the Provider)

本节中仍以字典数据表作为示例。

为了清晰起见,本节中调用ContentResolver.query()方法查询表中的数据都在UI线程中进行。但是在实际开发中,这应该是一个异步操作——方法应在子线程中进行(should do queries asynchronously on a separate thread)。方法之一是使用CursorLoader类。有关CursorLoader的相关内容,您可以参考我翻译的文档:《Android官方文档之App Components(Loaders)》。

从provider中查询数据,应按照如下操作进行:

  1. 声明读取数据的访问权限(Request the read access permission for the provider);

  2. 发送查询请求(Define the code that sends a query to the provider) ;

声明权限(Requesting read access permission)

为了从provider中检索数据,您的应用需要在manifest中声明读的权限(read access permission):使用<uses-permission>标签包含,并指定需要访问的provider。当程序安装时,该权限被隐式授予。

在用户字典provider所在应用程序的manifest中定义了android.permission.READ_USER_DICTIONARY权限。若您的应用程序需要访问该provider,则需要声明这个权限。

构建查询条件(Constructing the query)

参考第一段代码片段,查询条件可以按照如下方式定义:


// 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 = {""};

下面的代码片段演示了 从UI界面中获取用户输入的单词,并在字典表中查询表中是否包含该单词,若包含,用Cursor对象返回该单词在表中所在行的信息(如键入频率等):

/*
 * 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

防止用户恶意输入(Protecting against malicious input)

若由provider管理的是一个SQL数据库,那么用户输入的某些内容可能会对数据库造成破坏。

考虑下面这种情况:

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

上面是用户输入的需要在表中查询的内容。

这种输入方式会给数据库带来隐患:用户可以输入诸如"nothing; DROP TABLE *;"的内容,当程序读入用户的输入时,mSelectionClause 变量中的内容将是var = nothing; DROP TABLE *;`,这会导致数据库中的这张表被删除!

为了避免上述情况发生,应使用"?"作为占位符,以分离SQL语句和条件筛选参数,因为这种以输入方式,系统将不再把用户的输入作为SQL语句的一部分,用户无法再恶意破坏数据库的内容( Because it’s not treated as SQL, the user input can’t inject malicious SQL)。方式如下:

// Constructs a selection clause with a replaceable parameter
String mSelectionClause =  "var = ?";
// Defines an array to contain the selection arguments
String[] selectionArgs = {""};
// Sets the selection argument to the user's input
selectionArgs[0] = mUserInput;

显示查询结果(Displaying query results)

client端的ContentResolver.query()方法返回一个Cursor对象,它指向了按筛选条件查询的表的结果。调用Cursor中的方法,您可以遍历结果中的每一行,并通过表中的字段名得到该行中的每一列信息,当provider中的数据发生改变时,Cursor中的数据自动改变(automatically update the object when the provider’s data changes),或者触发observer 对象中的方法( trigger methods in an observer object when the Cursor changes)。

若没有行(rows)能匹配查询的条件,Cursor对象的Cursor.getCount()方法将返回0。

若查询过程发生了错误,Cursor对象将返回null,或者抛出一个异常。

由于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);

!请注意:为了使ListView能显示Cursor中的数据,Cursor中必须有一列为_ID字段,即使ListView中不显示该列,Cursor也必须包含该列。所以在建表时,应为表设置_ID字段。

从Cursor中获取数据(Getting data from query results)

下面代码演示了如何从Cursor中获取word字段中的数据。

// 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.
}

Content Provider 权限(Content Provider Permissions)

在应用程序中,需要为provider设置权限。其他应用程序只有获得了权限,才能访问provider。为provider设置权限保证了provider知晓访问它的应用程序想要获取什么数据(ensure that the user knows what data an application will try to access)。这保证了provider所管理的数据的安全。

如果provider未指定任何权限,其他任何应用将无法访问(If a provider’s application doesn’t specify any permissions, then other applications have no access to the provider’s data)。不过,无论provider是否制定了权限,与provider处于同一应用中的组件有完全的读写provider的权限(components in the provider’s application always have full read and write access, regardless of the specified permissions)。

正如上面提到的,用户字典provider需要android.permission.READ_USER_DICTIONARY权限才能被其他应用程序访问,当然,该权限只是具有了读取provider中数据的能力,如还需对provider所管理的数据进行增、删、改 等操作( inserting, updating, or deleting data),还需要声明android.permission.WRITE_USER_DICTIONARY权限。

声明这些权限需要在manifest中的<uses-permission>中进行。这些声明的权限将在应用安装时以列表的形式呈献给用户,若用户安装了应用,则用户默许了列表中的权限;否则,用户反对列表中的任何权限,程序都将安装失败。

声明读取(read)用户字典provider的权限如下:

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

增删改数据(Inserting, Updating, and Deleting Data)

与查询类似,使用resolver同样可以对provider进行增删改操作:

插入数据(Inserting data)

调用ContentResolver.insert()方法可向provider中插入数据。该方法向provider中的某个表中插入一行数据,并将该行数据的内容URI(content 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.putNull()传入空值。

在增加一行数据时,无需指定_ID字段的值,因为一般情况下,将_ID字段设为主键(primary key)和自增长(auto increment)。该字段将被自动赋值。

返回的newUri的格式如下:

content://user_dictionary/words/<id_value>

上面的<id_value>表示新加入的_ID号,如需从返回的URI对象中获取该_ID值,可以调用ContentUris.parseId()方法。

更新数据(Updating data)

在client段调用ContentResolver.update()方法可以修改一条数据,您同样可以使用ContentValues对象修改数据,如需将某一字段下的数据清空,直接传入null即可。

下面演示了将provider管理的用户字典表中的locale字段中含有"en"的数据条目值清空,返回值是修改过的行数。

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

删除数据(Deleting data)

与查询类似,调用ContentResolver.update()方法删除表中指定的数据,下面演示了删除表的appid的字段中包含user的条目。

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

Provider管理的数据类型(Provider Data Types)

除了用户字典表中的text类型,provider还支持下列类型的操作:

  • integer;

  • long integer (long);

  • floating point;

  • long floating point (double)。

provider还经常使用Binary Large OBject (BLOB)类型,它是一个最大容量为64KB的字节数组(64KB byte array)。您可以查询Cursor类中的若干”get”方法来查看provider支持的类型。

provider为每一个访问或返回client端的内容URI(content URI)对应了一个MIME数据类型(Providers also maintain MIME data type information for each content URI they define),通过该MIME数据类型可以判断您的应用程序是否支持该类型。当provider提供了复杂的数据类型时,您可能需要MIME数据类型的帮忙(You usually need the MIME type when you are working with a provider that contains complex data structures or files)。如,通讯录应用程序中的contact provider包含一张叫做ContactsContract.Data的表,在该表中的每一行,就是通过MIME类型标记联系人的数据信息。如需获得content URI中的MIME数据类型,可以调用ContentResolver.getType()方法。

访问Provider的其他方式(Alternative Forms of Provider Access)

除了上面介绍的访问content provider的方式以外,还有三种方式:

  • 批量访问(Batch access):您可以使用ContentProviderOperation类对provider进行批量访问。并在client调用ContentResolver.applyBatch()方法实现批量操作。
  • 异步查询(Asynchronous queries):您应当在子线程中执行查询操作,可以使用CursorLoader类来实现。
  • 通过Intent访问数据(Data access via intents):虽然不能直接向provider传递intent,但是可以向provider所在的应用传递。这种方式提供了修改provider中数据的最齐全的方法(which is usually the best-equipped to modify the provider’s data)。

批量访问(Batch access)

当需要对表中的多行数据、或需要对多表进行操作时,这需要批量操作。除此之外,实现数据库的事务操作(原子操作,an atomic operation)也需要批量操作。为了实现批量模式,需要一组ContentProviderOperation对象,并在client端调用ContentResolver.applyBatch()方法,向该方法中传入provider的authority,而不是content URI,因为这可以对不同表进行操作。ContentResolver.applyBatch()方法将返回一组结果。

使用Intent访问数据(Data access via intents)

获取临时访问权限(Getting access with temporary permissions)

向应用程序发送intent,即便没有访问该应用程序中provider的权限,您依然可以访问provider的数据。甚至通过返回的intent还能获得URI权限( receiving back a result intent containing “URI” permissions)。您可以设置flag获取返回的intent中的权限:

  • 读取权限:FLAG_GRANT_READ_URI_PERMISSION

  • 写入权限:FLAG_GRANT_WRITE_URI_PERMISSION

provider在manifest中通过android:grantUriPermission属性、以及provider的子标签<grant-uri-permission>定义了URI权限。

下面的例子演示了如何从通讯录应用中的provider管理的表中获取联系人信息(无需声明READ_CONTACTS权限):

  • 调用startActivityForResult()方法,传入IntentIntent应包含ACTION_PICK的action,和contact的MIME类型CONTENT_ITEM_TYPE

  • 这会启动通讯录应用的selectionactivity,该activity会切至前台;

  • selectionactivity中,用户会选择某个联系人并修改其信息,此时,setResult(resultcode, intent)方法将被回调,并将intent回传至您的应用中,intent中包含了用户选择的联系人的content URI,并在extra中包含了FLAG_GRANT_READ_URI_PERMISSIONflag。该flag提供了您的应用程序访问联系人通讯中指定数据的权限(These flags grant URI permission to your app to read data for the contact pointed to by the content URI)。最后,selectionactivity调用finish()destroy。

  • 您的activity切回前台并回调onActivityResult()方法获取回传的intent,从Intent中获取的content URI可以直接访问联系人应用中的provider数据而无需在manifest中声明权限。

MIME类型引用(MIME Type Reference)

provider可以返回标准或定制的MIME类型的字符串:

MIME类型的标准形式:

type/subtype

比如,常用的MIME类型text/html,表示其指向的数据包含text类型和html子类型。若provider返回的URI中包含的MIME类型为text/html,那么通过该URI返回的结果将包含HTML标签。

定制的MIME类型,也称作vendor-specific MIME 类型,它具有更复杂的结构:

其中type类型中:
vnd.android.cursor.dir表示URI指向的数据包含多行。而vnd.android.cursor.item表示包含一行。

subtype时provider定制的(provider-specific),系统自带的provider通常包含一个简单的subtype,如通讯录应用中增加一行电话号码的记录,那么指向它的URI中的MIME类型将为:

vnd.android.cursor.item/phone_v2

你可能感兴趣的:(sql,Provider,query,MIME,resolver)