使用 Content Provider | Android
使用 Content Provider
如果你要公开你的数据,你可以创建或者使用一个Content Provider。它是一个能使所以应用程度都能存储和检索数据的对象。它是唯一在包和包之间分享数据的方法;因为不存在那种供所有的包来共享的一般存储空间。Android自带了一些Content Provider,它们用于一些一般的数据类型(音频,视频,图片,个人联系信息等等)。您能从 provider 这个包中看到一些Android自带的Content Provider。到底表面之下一个Content Provider是如何存储数据的决定于这个Content Provider是如何实现的,但是所有的Content Provider必须实现一种一般公约用来数据查询和一种一般公约来返回结果。然而,一个Content Provider能够实现自定义的方法,使得在处理一些特定的数据时,对于数据的存储/检索更加简单。
这个文档包含了关开Content Provider的两个主题。
- 使用一个Content Provider
- 创建一个Content Provider
使用一个Content Provider来在存储和检索数据
这个部分描述了使用一个您自己或者他人创建的Content Provider来存储和检索数据。Android提供了若干个Content Provider用于很广泛的数据类型,从音乐,图片文件到电话号码。您可以看到在 android.provider 这个包中,看到Content Provider的列表。Android的Content Provider和他们的用户?联系很松散。每个Content Provider提供一个唯一的字符串(一个URI)来确定它将要处理的数据类型,并且用户?必须使用这个字符串来存储和检索这个类型的数据。我们将在 Querying for Data 中更多的解释这一点。
- 数据查询
- 执行一个查询
- 查询返回什么
- 文件的查询
- 读取检索数据
- 修改数据
- 加入一条记录
- 删除一条记录
数据查询
每个Content Provider提供一个唯一的公开的URI(由android.net.Uri封装?),它被用户用来在这个Content Provider上查询/添加/更新/删除数据。URI有两种形式:一种显示某种数据类型的所有的值(比如,所有的个人联系信息),还有一种显示某种数据类型的一个特定的记录(比如,Joe Smith的个人信息)- content://contacts/people/ 是第一种形式的URI,它会返回设备上所有的联系人名字
- content://contacts/people/23 是第二种形式的URI,只返回ID=23的那行
一个应用程序发送一个查询到设备用于检索,这个查询是指定一般类型的对象(所有的电话号码),或者是一个具体的对象(Bob的电话号码)。然后Android会返回一个包含有结果记录集合,特定列集合的游标(?)。我们来看一个假设的查询字符串和返回结果集合(为了更加清晰,结果已经经过裁剪)
query = content://contacts/people/
Results:
_ID | _COUNT | NUMBER | NUMBER_KEY | LABEL | NAME | TYPE |
---|---|---|---|---|---|---|
13 | 4 | (425) 555 6677 | 425 555 6677 | California office | Bully Pulpit | Work |
44 | 4 | (212) 555-1234 | 212 555 1234 | NY apartment | Alan Vain | Home |
45 | 4 | (212) 555-6657 | 212 555 6657 | Downtown office | Alan Vain | Work |
53 | 4 | 201.555.4433 | 201 555 4433 | Love Nest | Rex Cars | Home |
注意,查询字符串并不是一个标准的SQL查询,但是相对的,一个URI字符串描述了要返回的数据的类型。这个URI由三个部分组成:1,字符串"content://";2,一段字条串,描述了要返回什么类型的数据;3,一个可选的特定类型特定的ID。下面是几个查询字符串的例子:
- content://media/images 一个URI,它会返回设备上所有图片的列表。
- content://contacts/people/ 一个URI,它会返回设备上所有的联系人名字的列表。
- content://contacts/people/23 一个URI,它会返回联系人ID=23的那一行。
虽然说有一种一般的形式,但是查询URI还是有点随意和让人迷惑。因此,Android在 android.provider 包中提供了一系列的辅助类,它们定义了这些查询字符串,所以您不必知道不同数据类型真正的URI值。这些辅助类定义了一个叫CONTENT_URI的字符串(实际上,一个叫 Uri 的包装类)。比如, android.provider.contacts.People.CONTENT_URI 定义了用于在Andoid自带的people content provider中查找联系人的查询字符串。
典型的,你会用这个已定义的CONTENT_URI对象来执行一个查询。唯一的一种情况,你需要自己检查或者修改这个字符串,就是你要在这个URI的结尾加上一个ID来检索某个特定的记录的时候。所以,举个例子,如果你要查找联系人中 ID=23的记录,你可能会执行下面的一个查询:
// Get the base URI for contacts.
Uri myPerson = android.provider.Contacts.People.CONTENT_URI;
// Add the ID of the record I'm looking for (I'd have to know this somehow).
myPerson.addId(23);
// Query for this record.
Cursor cur = managedQuery(myPerson, null, null, null);
这个查询返回一个数据库查询结果集的游标。哪列被返回了,它们叫什么名字接下来再讨论。现在,只要知道你可以指定特定的列返回,指定排列顺序,还有一个SQL WHERE子句。
你应该使用 Activity.managedQuery() 方法去检索一个被管理的游标。一个被管理的游标会处理所有细微的东西,像应用程序停止时的自我卸载,还有在应用程序重新启动时的自我查询。你可以调用 Activity.startManagingCursor() 让Android去管理一个没有被管理的游标。
来看一个检索联系人名字,电话号码和照片列表的查询的例子。
// An array specifying which columns to return.
// The provider exposes a list of column names it returns for a specific
// query, or you can get all columns and iterate through them.
string[] projection = new string[] {
android.provider.BaseColumns._ID,
android.provider.Contacts.PeopleColumns.NAME,
android.provider.Contacts.PhonesColumns.NUMBER,
android.provider.Contacts.PeopleColumns.PHOTO
};
// Best way to retrieve a query; returns a managed query.
Cursor managedCursor = managedQuery( android.provider.Contacts.Phones.CONTENT_URI,
projection, //Which columns to return.
null, // WHERE clause--we won't specify.
android.provider.Contacts.PeopleColumns.NAME + " ASC"); // Order-by clause.
这个查询会检索所有存放在phone number contacts provider里的电话号码。它会检索每个联系人的名字,号码和唯一记录ID。然后我们可以浏览返回的结果,往前或者往后,删除记录,添加记录和修改记录。我们会在下一个部分要讲到这些。
查询返回什么
一个查询返回零个或者多个数据库记录的集合。列名,排列顺序和类型对于每个Content Provider来说都是不同的,但是每个查询都包含一列叫做 _id,它表示那一列所在的记录的ID。如果一个查询可以返回二进制数据,比如一个点阵图或者一个音频文件,它会有叫content:// 这样URI的名字的一列,你可以它来得到数据(更多的关于如何得到文件的信息我们会在后面讲到)。看下面的例子:
_id | name | number | photo |
---|---|---|---|
44 | Alan Vain | 212 555 1234 | content://images/media/123 |
13 | Bully Pulpit | 425 555 6677 | content://images/media/128 |
53 | Rex Cars | 201 555 4433 | content://images/media/332 |
这个结果集演示了当我们指定一个列的子集的时候什么会被返回。可选的子集的列表在查询的projection参数中定义。一个Content Manager应该列出它支持哪些列,可以用实现一系列的接口来描述每一列(看 Contacts.People.Phones ,它从 BaseColumns , PhonesColumns 和 PeopleColumns 继承而来),或者用常量的形式列出列的名字。注意,你需要知道一个列的数据类型,为了能够读取它;
读取数据的方法取决于数据的类型,and a column's data type is not exposed programmatically.(?)
检索后的数据同一个 Cursor 管理(?),它可以被用来在结果集合中向前向后迭代。您可以用这个游标来读取,修改,或者删除行。
添加一条新行需要一个不同的对象,这个会马上在后面讲到。
注意,按照惯例,每个记录集合包括一个叫 _id 的域,它是一个特定记录的ID,还有一个叫 _count 的域,它是一个描述当时结果集合中有多少个记录的记数器。这些域名在 BaseColumns 被定义。
文件查询(?)
前面那个查询结果演示了在一个数据集合中,一个文件是怎么样被返回的。典型的,一个文件域是那个文件的字符串路径(但是不是一定要是这样)。然而,一定调用应该永远不要试图直接读取和打开文件(一个原因是权限问题会导致失败)。你应该调用 ContentResolver.openInputStream() / ContentResolver.openOutputStream()方法,或者调用Content Provider提供的辅助方法。
读取检索数据
如果你读取的是一个二进制数据,比如一个图片文件,你应该调用 ContentResolver.openOutputStream() ,参数是列中的 content:// URI字符串。
下面这个片段演示了怎么从我们的电话号码查询读取名字和电话号码:
private void getColumnData(Cursor cur){
if (cur.first()) {
String name;
String phoneNumber;
int nameColumn = cur.getColumnIndex(android.provider.Contacts.PeopleColumns.NAME);
int phoneColumn = cur.getColumnIndex(android.provider.Contacts.PhonesColumns.NUMBER);
int pathColumn = cur.getColumnIndex(android.provider.Contacts.PeopleColumns.PHOTO);
String imagePath;
do {
// Get the field values
name = cur.getString(nameColumn);
phoneNumber = cur.getString(phoneColumn);
imagePath = cur.getString(stringColumn);
InputStream is = getContentResolver().openInputStream(imagePath);
... read the file stream into something...
is.close()
// Do something with the values.
...
} while (cur.next());
}
}
注意,图片域实际上只是一个字符串的值。一些Content Provider类提供了辅助方法使得从这些文件读取域值变得更加简单。记住,每当你在Cursor类调用更新方法,你必须调用 commitUpdates() 来发送变更到数据库。
修改数据
要改变一个单独的记录,设定游标(Cursor)到一个合适的对象,调用合适的更新...方法,然后调用commitUpdates()。
要批量的改变一组记录(比如,在所有的联系人域中反"NY"改为"New York"),调用ContentResolver.update()方法,参数是要改变的列名和值。
记住,每当你在Cursor类调用更新方法,你必须调用 commitUpdates()来发送变更到数据库。
添加一个新记录
要添加一个新记录,调用ContentResolver.insert()方法,参数是要添加记录类型的URI,和你要加到新记录里的值的一个表示(?)。它会返回新记录的整个URI,包括记录号,然后你可以用这个记录号去查询和得到新记录的一个游标(Cursor)。
记住,每当你在Cursor类调用更新方法,你必须调用 commitUpdates()来发送变更到数据库。
要存储一个文件,你可以调用 ContentResolver().openOutputStream(),参数是下列这个片段里展示出来的URI:
// Save the name and description in a map. Key is the content provider's
// column name, value is the value to save in that record field.
HashMap<String, Object> values = new HashMap<String, Object>();
values.put(Media.Images.NAME, "road_trip_1");
values.put(Media.Images.DESCRIPTION, "Day 1, trip to Los Angeles");
// Add a new record without the bitmap, but with the values.
// It returns the URI of the new record.
Uri uri = getContentResolver().insert(Media.Images.CONTENT_URI, values);
// Now get a handle to the file for that record, and save the data into it.
// sourceBitmap is a Bitmap object representing the file to save to the database.
OutputStream outStream = getContentResolver.openOutputStream(uri);
sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
删除记录
要删除一个单独的记录,调用 ContentResolver.delete()方法,参数是那一行的URI,或者调用Cursor.deleteRow()。
要删除多行,调用ContentResolver.delete()方法,参数是要删除记录类型的URI(比如,android.providr.Contacts.People.CONTENT_URI)和一个SQL WHERE子句,这个子句定义哪些行要被删除(警告:如果用ContentResolver.delete()删除一个一般类型的记录,要确保包含一个有效的WHERE子句,否则你就要冒着删除比你想删除的行更多的行的风险!)。
记住,每当你在Cursor类调用更新方法,你必须调用 commitUpdates()来发送变更到数据库。
创建一个Content Provider
下面是你如何创建你自己的Content Provider,将它作为公共资源用于读取和写入一个的数据类型的步骤:
- 继承ContentProvider。
- 定义一个 public static
Uri,取名叫做CONTENT_URI。这个字符串代表了你的Content Provider要处理的"content://"。你必须为这个值定义一个唯一的字符串;最好的解决方法是用
the fully-qualified class name of your content provider (lowercase).所以,像这样:public static final Uri CONTENT_URI = Uri.parse( "content://com.google.codelab.rssprovider");
- 创建你存储数据的系统。大多数的Content Provider使用Android的文件存储方法或者SQLite数据库来存储数据,但是你可以用你想用的任何方法来存储数据,只要你遵循调用和返回值的惯例。如果你使用SQLite,Android 提供了DatabaseContentProvider 和 SQLiteOpenHelper 类来帮助你。
- 定义你会返回给客户端的列的名字。如果你使用一个后台的数据库,这些列的名字通常和SQL数据库里的列的名字一样。在任何情况下,你都应该包含一个整数的叫 _id 的列,用来定义一个特定的记录号。如果使用SQLite数据库,它应该是这种类型:
INTEGER PRIMARY KEY AUTOINCREMENT。
AUTOINCREMENT 描述符是可选的,但是默认情况下,SQLite 自动递增ID域到一下个大于表中现已存在的最大的ID的值。如果你删除了最后一行,那么新加入的下一行会和被删除的那行具有相同的ID。为了避免这种情况,让SQLite的递增到下一个最大值不论删除与否,那么给你的ID列赋与接下来的那个类型:
INTEGER PRIMARY KEY AUTOINCREMENT。(注意,你应该包含一个唯一的 _id 域,不管你是不是有其他的域(比如一个URL)能在记录中保持唯一。)Android 提供了 SQLiteOpenHelper 方法来帮助你创建和管理你的数据库的版本。 - 如果你要处理字节数据,比如点阵图文件,存储这个数据的域实际上应该是一个包含那个文件的 content:// URI 的字符串。用户会用这个域来检索数据。对于这种类型的数据,Content Provider(可以是同一个Content Provider也可以是另外一个Content Provider──比如,如果你存储一张照片你会使用 media content provider)应该为那个记录实现一个叫 _data的域。这个 _data 域列出了文件的在设备上的确切路径。这个域并不是让客户端来读取的,是给 ContentResolver的。客户端会调用ContentResolver.openOutputStream() 方法,用URI作为参数(比如,名叫photo的列可能会有一个值: content://media/iages/4453)。ContentResolver会请求记录的 _data域,而且因为它有比客户端更高的权限,它能直接访问那个文件并且返回给客户端一个那个文件的读取包。
- 声明客户端可以用来指定哪些列可以返回的 public static 的字符串。或者从游标(Cursor)来指定域名。仔细地文档每个域的数据类型。记住,文件域,像音频,点阵图域,一般返回路径字符串值。(??????)
- 对于一个查询,返回一个结果集合的Cursor 对象。这意味着要实现 query(),update(),insert(),和delete()方法。作为礼貌,你可能会想调用ContentResolver.notifyChange() 来提醒监听者关于更新的信息。
- 加一个 <provider>标签到 AndroidMnaifest.xml,使用它的authorities属性定义一个authority part of the content type it should handle. For example, if your content type is content://com.example.autos/auto to request a list of all autos, then authorities would be
com.example.autos
. Set the multiprocess attribute to true if data does not need to be synchronized between multiple running versions of the content provider. - 如果你处理的是一种新的数据类型,你必须定义一个新的MIME
- E
- De
- Cre
- Def
- If
- Dec
- Re
- Add a
<provider>
tag to AndroidManifest.xml, and use its authorities attribute to define the authority part of the content type it should handle. For example, if your content type is content://com.example.autos/auto to request a list of all autos, then authorities would becom.example.autos
. Set the multiprocess attribute to true if data does not need to be synchronized between multiple running versions of the content provider. - If you are handling a new data type, you must define a new MIME type to return for your implementation of android.ContentProvider.getType(url). This type corresponds to the
content://
URI submitted to getType(), which will be one of the content types handled by the provider. The MIME type for each content type has two forms: one for a specific record, and one for multiple records. Use the Uri methods to help determine what is being requested. Here is the general format for each:vnd.android.cursor.item/vnd.yourcompanyname.contenttype
for a single row. For example, a request for train record 122 usingcontent://com.example.transportationprovider/trains/122
might return the MIME typevnd.android.cursor.item/vnd.example.rail
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
for multiple rows. For example, a request for all train records usingcontent://com.example.transportationprovider/trains
might return the MIME typevnd.android.cursor.dir/vnd.example.rail
For an example of a private content provider implementation, see the NodePadProvider class in the notepad sample application that ships with the SDK.
Here is a recap of the important parts of a content URI:
- Standard required prefix. Never modified.
- Authority part. For third-party applications, this should be a fully-qualified class to ensure uniqueness. This corresponds to the value in the
<provider>
element's authorities attribute:<provider class="TransportationProvider" authorities="com.example.transportationprovider" />
- The path that the content provider uses to determine what kind of data is being requested. This can be zero or more segments: if the content provider exposes only one type of data (only trains, for example), this can be absent. If it provides several types, including subtypes, this can be several elements long: e.g., "
land/bus
,land/train
,sea/ship
, andsea/submarine
" to give four possibilities. - A specific record being requested, if any. This is the _id value of a specific record being requested. If all records of a specific type are being requested, omit this and the trailing slash:
content://com.example.transportationprovider/trains