内容提供器Content Providers
内容提供器用来存放和获取数据并使这些数据可以被所有的应用程序访问。它们是应用程序之间共享数据的唯一方法;不存在所有Android软件包都能访问的公共储存区域。
Android为常见数据类型(音频,视频,图像,个人联系人信息,等等)装载了很多内容提供器。你可以看到在android.provider包里列举了一些。你还能查询这些提供器包含了什么数据(尽管,对某些提供器,你必须获取合适的权限来读取数据)。
如果你想公开你自己的数据,你有两个选择:你可以创建你自己的内容提供器(一个ContentProvider子类)或者你可以给已有的提供器添加数据-如果存在一个控制同样类型数据的内容提供器且你拥有写的权限。
这篇文档是一篇关于如何使用内容提供器的简介。先是一个简短的基础知识讨论,然后探究如何查询一个内容提供器,如何修改内容提供器控制的数据,以及如何创建你自己的内容提供器。
内容提供器的基础知识Content Provider Basics
内容提供器究竟如何在表层下保存它的数据依赖于它的设计者。但是所有的内容提供器实现了一个公共的接口来查询这个提供器和返回结果-增加,替换,和删除数据也是一样。
这是一个客户端直接使用的接口,一般是通过ContentResolver对象。你可以通过getContentResolver()从一个活动或其它应用程序组件的实现里获取一个ContentResolver:
ContentResolver cr = getContentResolver();
然后你可以使用这个ContentResolver的方法来和你感兴趣的任何内容提供器交互。
当初始化一个查询时,Android系统识别查询目标的内容提供器并确保它正在运行。系统实例化所有的ContentProvider对象;你从来不需要自己做。事实上,你从来不会直接处理ContentProvider对象。通常,对于每个类型的ContentProvider只有一个简单的实例。但它能够和不同应用程序和进程中的多个ContentProvider对象通讯。进程间的交互通过ContentResolver和ContentProvider类处理。
数据模型The data model
内容提供器以数据库模型上的一个简单表格形式暴露它们的数据,这里每一个行是一个记录,每一列是特别类型和含义的数据。比如,关于个人信息以及他们的电话号码可能会以下面的方式展示:
_ID | NUMBER | NUMBER_KEY | LABEL | NAME | TYPE |
13 | (425) 555 6677 | 425 555 6677 | Kirkland office | Bully Pulpit | TYPE_WORK |
44 | (212) 555-1234 | 212 555 1234 | NY apartment | Alan Vain | TYPE_HOME |
45 | (212) 555-6657 | 212 555 6657 | Downtown office | Alan Vain | TYPE_MOBILE |
53 | 201.555.4433 | 201 555 4433 | Love Nest | Rex Cars | TYPE_HOME |
每个记录包含一个数字的_ID字段用来唯一标识这个表格里的记录。IDs可以用来匹配相关表格中的记录-比如,用来在一张表格中查找个人电话号码并在另外一张表格中查找这个人的照片。
一个查询返回一个Cursor 对象可在表格和列中移动来读取每个字段的内容。它有特定的方法来读取每个数据类型。所以,为了读取一个字段,你必须了解这个字段包含了什么数据类型。(后面会更多的讨论查询结果和游标Cursor对象)。
唯一资源标识符URIs
每个内容提供器暴露一个公开的URI(以一个Uri 对象包装)来唯一的标识它的数据集。一个控制多个数据集(多个表)的内容提供器为每一个数据集暴露一个单独的URI。所有提供器的URIs以字符串"content://"开始。这个content:形式表明了这个数据正被一个内容提供器控制着。
如果你正准备定义一个内容提供器,为了简化客户端代码和使将来的升级更清楚,最好也为它的URI定义一个常量。Android为这个平台所有的提供器定义了CONTENT_URI 常量。比如,匹配个人电话号码的表的URI和包含个人照片的表的URI是:(均由联系人Contacts内容提供器控制)
android.provider.Contacts.Phones.CONTENT_URI
android.provider.Contacts.Photos.CONTENT_URI
类似的,最近电话呼叫的表和日程表条目的URI如下:Similarly, the URIs for the table of recent phone calls and the table of calendar entries are:
android.provider.CallLog.Calls.CONTENT_URI
android.provider.Calendar.CONTENT_URI
这个URI常量被使用在和这个内容提供器所有的交互中。每个ContentResolver 方法采用这个URI作为它的第一个参数。正是它标识了ContentResolver应该和哪个内容提供器对话以及这个内容提供器的哪张表格是其目标。
查询一个内容提供器Querying a Content Provider
你需要三方面的信息来查询一个内容提供器:
· 用来标识内容提供器的URI
· 你想获取的数据字段的名字
· 这些字段的数据类型
如果你想查询某一条记录,你同样需要那条记录的ID。
生成查询Making the query
你可以使用ContentResolver.query()方法或者Activity.managedQuery()方法来查询一个内容提供器。两种方法使用相同的参数序列,而且都返回一个Cursor对象。不过,managedQuery()使得活动需要管理这个游标的生命周期。一个被管理的游标处理所有的细节,比如当活动暂停时卸载自身,而活动重新启动时重新查询它自己。你可以让一个活动开始管理一个尚未被管理的游标对象,通过如下调用: Activity.startManagingCursor()。
无论query()还是managedQuery(),它们的第一个参数都是内容提供器的URI-CONTENT_URI常量用来标识某个特定的ContentProvider和数据集(参见前面的URIs)。
为了限制只对一个记录进行查询,你可以在URI后面扩展这个记录的_ID值-也就是,在URI路径部分的最后加上匹配这个ID的字符串。比如,如果ID是23,那么URI会是:
content://. . . ./23
有一些辅助方法,特别是ContentUris.withAppendedId() 和Uri.withAppendedPath(),使得为URI扩展一个ID变得简单。所以,比如,如果你想在联系人数据库中查找记录23,你可能需要构造如下的查询语句:
import android.provider.Contacts.People;
import android.content.ContentUris;
import android.net.Uri;
import android.database.Cursor;
// Use the ContentUris method to produce the base URI for the contact with _ID == 23.
Uri myPerson = ContentUris.withAppendedId(People.CONTENT_URI, 23);
// Alternatively, use the Uri method to produce the base URI.
// It takes a string rather than an integer.
Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "23");
// Then query for this specific record:
Cursor cur = managedQuery(myPerson, null, null, null, null);
query() 和managedQuery()方法的其它参数限定了更多的查询细节。如下:
· 应该返回的数据列的名字。null值返回所有列。否则只有列出名字的列被返回。所有这个平台的内容提供器为它们的列定义了常量。比如,android.provider.Contacts.Phones类对前面说明过的通讯录中各个列的名字定义了常量ID, NUMBER, NUMBER_KEY, NAME, 等等。
· 指明返回行的过滤器,以一个SQL WHERE语句格式化。 null值返回所有行。(除非这个URI限定只查询一个单独的记录)。
· 选择参数
· 返回行的排列顺序,以一个SQL ORDER BY语句格式化(不包含ORDER BY本身)。null值表示以该表格的默认顺序返回,有可能是无序的。
让我们看一个查询的例子吧,这个查询获取一个联系人名字和首选电话号码列表:
import android.provider.Contacts.People;
import android.database.Cursor;
// Form an array specifying which columns to return.
String[] projection = new String[] {
People._ID,
People._COUNT,
People.NAME,
People.NUMBER
};
// Get the base URI for the People table in the Contacts content provider.
Uri contacts = People.CONTENT_URI;
// Make the query.
Cursor managedCursor = managedQuery(contacts,
projection, // Which columns to return
null, // Which rows to return (all rows)
null, // Selection arguments (none)
// Put the results in ascending order by name
People.NAME + " ASC");
这个查询从联系人内容提供器中获取了数据。它得到名字,首选电话号码,以及每个联系人的唯一记录ID。同时它在每个记录的_COUNT字段告知返回的记录数目。
列名的常量被定义在不同的接口中-_ID和_COUNT 定义在BaseColumns里, NAME在PeopleColumns里,NUMBER在PhoneColumns里。Contacts.People类已经实现了这些接口,这就是为什么上面的代码实例只需要使用类名就可以引用它们的原因。
查询的返回结果What a query returns
一个查询返回零个或更多数据库记录的集合。列名,默认顺序,以及它们的数据类型是特定于每个内容提供器的。但所有提供器都有一个_ID列,包含了每个记录的唯一ID。另外所有的提供器都可以通过返回_COUNT 列告知记录数目。它的数值对于所有的行而言都是一样的。
下面是前述查询的返回结果的一个例子:
_ID | _COUNT | NAME | NUMBER |
44 | 3 | Alan Vain | 212 555 1234 |
13 | 3 | Bully Pulpit | 425 555 6677 |
53 | 3 | Rex Cars | 201 555 4433 |
获取到的数据通过一个游标Cursor对象暴露出来,通过游标你可以在结果集中前后浏览。你只能用这个对象来读取数据。如果想增加,修改和删除数据,你必须使用一个ContentResolver对象。
读取查询所获数据Reading retrieved data
查询返回的游标对象可以用来访问结果记录集。如果你通过指定的一个ID来查询,这个集合将只有一个值。否则,它可以包含多个数值。(如果没有匹配结果,那还可能是空的。)你可以从表格中的特定字段读取数据,但你必须知道这个字段的数据类型,因为这个游标对象对于每种数据类型都有一个单独的读取方法-比如getString(), getInt(), 和getFloat()。(不过,对于大多数类型,如果你调用读取字符串的方法,游标对象将返回给你这个数据的字符串表示。)游标可以让你按列索引请求列名,或者按列名请求列索引。
下面的代码片断演示了如何从前述查询结果中读取名字和电话号码:
import android.provider.Contacts.People;
private void getColumnData(Cursor cur){
if (cur.moveToFirst()) {
String name;
String phoneNumber;
int nameColumn = cur.getColumnIndex(People.NAME);
int phoneColumn = cur.getColumnIndex(People.NUMBER);
String imagePath;
do {
// Get the field values
name = cur.getString(nameColumn);
phoneNumber = cur.getString(phoneColumn);
// Do something with the values.
...
} while (cur.moveToNext());
}
}
如果一个查询可能返回二进制数据,比如一个图像或声音,这个数据可能直接被输入到表格或表格条目中也可能是一个content: URI的字符串可用来获取这个数据,一般而言,较小的数据(例如,20到50K或更小)最可能被直接存放到表格中,可以通过调用Cursor.getBlob()来获取。它返回一个字节数组。
如果这个表格条目是一个content: URI,你不该试图直接打开和读取该文件(会因为权限问题而失败)。相反,你应该调用ContentResolver.openInputStream()来得到一个InputStream对象,你可以使用它来读取数据。
保存在内容提供器中的数据可以通过下面的方法修改:
· 增加新的记录
· 为已有的记录添加新的数据
· 批量更新已有记录
· 删除记录
所有的数据修改操作都通过使用ContentResolver方法来完成。一些内容提供器对写数据需要一个比读数据更强的权限约束。如果你没有一个内容提供器的写权限,这个ContentResolver方法会失败。
增加记录Adding records
想要给一个内容提供器增加一个新的记录,第一步是在ContentValues对象里构建一个键-值对映射,这里每个键和内容提供器的一个列名匹配而值是新记录中那个列期望的值。然后调用ContentResolver.insert()并传递给它提供器的URI和这个ContentValues映射图。这个方法返回新记录的URI全名-也就是,内容提供器的URI加上该新记录的扩展ID。你可以使用这个URI来查询并得到这个新记录上的一个游标,然后进一步修改这个记录。下面是一个例子:
import android.provider.Contacts.People;
import android.content.ContentResolver;
import android.content.ContentValues;
ContentValues values = new ContentValues();
// Add Abraham Lincoln to contacts and make him a favorite.
values.put(People.NAME, "Abraham Lincoln");
// 1 = the new contact is added to favorites
// 0 = the new contact is not added to favorites
values.put(People.STARRED, 1);
Uri uri = getContentResolver().insert(People.CONTENT_URI, values);
增加新值Adding new values
一旦记录已经存在,你就可以添加新的信息或修改已有信息。比如,上例中的下一步就是添加联系人信息-如一个电话号码或一个即时通讯IM或电子邮箱地址-到新的条目中。
在联系人数据库中增加一条记录的最佳途径是在该记录URI后扩展表名,然后使用这个修正的URI来添加新的数据值。为此,每个联系人表暴露一个CONTENT_DIRECTORY常量的表名。下面的代码继续之前的例子,为上面刚刚创建的记录添加一个电话号码和电子邮件地址:
Uri phoneUri = null;
Uri emailUri = null;
// Add a phone number for Abraham Lincoln. Begin with the URI for
// the new record just returned by insert(); it ends with the _ID
// of the new record, so we don't have to add the ID ourselves.
// Then append the designation for the phone table to this URI,
// and use the resulting URI to insert the phone number.
phoneUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY);
values.clear();
values.put(People.Phones.TYPE, People.Phones.TYPE_MOBILE);
values.put(People.Phones.NUMBER, "1233214567");
getContentResolver().insert(phoneUri, values);
// Now add an email address in the same way.
emailUri = Uri.withAppendedPath(uri, People.ContactMethods.CONTENT_DIRECTORY);
values.clear();
// ContactMethods.KIND is used to distinguish different kinds of
// contact methods, such as email, IM, etc.
values.put(People.ContactMethods.KIND, Contacts.KIND_EMAIL);
values.put(People.ContactMethods.DATA, "test@example.com");
values.put(People.ContactMethods.TYPE, People.ContactMethods.TYPE_HOME);
getContentResolver().insert(emailUri, values);
你可以通过调用接收字节流的ContentValues.put()版本来把少量的二进制数据放到一张表格里去。这对于像小图标或短小的音频片断这样的数据是可行的。但是,如果你有大量二进制数据需要添加,比如一张相片或一首完整的歌曲,则需要把该数据的content: URI放到表里然后以该文件的URI调用ContentResolver.openOutputStream() 方法。(这导致内容提供器把数据保存在一个文件里并且记录文件路径在这个记录的一个隐藏字段中。)
考虑到这一点,MediaStore 内容提供器,这个用来分发图像,音频和视频数据的主内容提供器,利用了一个特殊的约定:用来获取关于这个二进制数据的元信息的query()或managedQuery()方法使用的URI,同样可以被openInputStream()方法用来数据本身。类似的,用来把元信息放进一个MediaStore记录里的insert()方法使用的URI,同样可以被openOutputStream()方法用来在那里存放二进制数据。下面的代码片断说明了这个约定:
import android.provider.MediaStore.Images.Media;
import android.content.ContentValues;
import java.io.OutputStream;
// Save the name and description of an image in a ContentValues map.
ContentValues values = new ContentValues(3);
values.put(Media.DISPLAY_NAME, "road_trip_1");
values.put(Media.DESCRIPTION, "Day 1, trip to Los Angeles");
values.put(Media.MIME_TYPE, "image/jpeg");
// Add a new record without the bitmap, but with the values just set.
// insert() returns the URI of the new record.
Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, values);
// Now get a handle to the file for that record, and save the data into it.
// Here, sourceBitmap is a Bitmap object representing the file to save to the database.
try {
OutputStream outStream = getContentResolver().openOutputStream(uri);
sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
} catch (Exception e) {
Log.e(TAG, "exception while writing image", e);
}
批量更新记录Batch updating records
要批量更新一组记录(例如,把所有字段中的"NY"改为"New York"),可以传以需要改变的列和值参数来调用ContentResolver.update()方法。
删除一个记录Deleting a record
要删除单个记录,可以传以一个特定行的URI参数来调用ContentResolver.delete()方法。
要删除多行记录,可以传以需要被删除的记录类型的URI参数来调用ContentResolver.delete()方法(例如,android.provider.Contacts.People.CONTENT_URI)以及一个SQL WHERE 语句来定义哪些行要被删除。(小心:如果你想删除一个通用类型,你得确保包含一个合法的WHERE语句,否则你可能删除比设想的多得多的记录!)
创建一个内容提供器Creating a Content Provider
要创建一个内容提供器,你必须:
· 建立一个保存数据的系统。大多数内容提供器使用Android的文件储存方法或SQLite数据库来存放它们的数据,但是你可以用任何你想要的方式来存放数据。Android提供SQLiteOpenHelper类来帮助你创建一个数据库以及SQLiteDatabase类来管理它。
· 扩展ContentProvider类来提供数据访问接口。
· 在清单manifest文件中为你的应用程序声明这个内容提供器(AndroidManifest.xml)。
下面的章节对后来两项任务有一些标注。
扩展ContentProvider类Extending the ContentProvider class
你可以定义一个ContentProvider子类来暴露你的数据给其它使用符合ContentResolver和游标Cursor对象约定的应用程序。理论上,这意味需要实现6个ContentProvider类的抽象方法:
query()
insert()
update()
delete()
getType()
onCreate()
query()方法必须返回一个游标Cursor对象可以用来遍历请求数据,游标本身是一个接口,但Android提供了一些现成的Cursor对象给你使用。例如,SQLiteCursor可以用来遍历SQLite数据库。你可以通过调用任意的SQLiteDatabase类的query()方法得到它。还有一些其它的游标实现-比如MatrixCursor-用来访问没有存放在数据库中的数据。
因为这些内容提供器的方法可以从不同的进程和线程的各个ContentResolver对象中调用,所以它们必须以线程安全的方式来实现。
周到起见,当数据被修改时,你可能还需要调用ContentResolver.notifyChange()方法来通知侦听者。
除了定义子类以外,你应该还需要采取其它一些步骤来简化客户端的工作和让这个类更容易被访问:
· 定义一个public static final Uri 命名为CONTENT_URI。这是你的内容提供器处理的整个content: URI的字符串。你必须为它定义一个唯一的字符串。最佳方案是使用这个内容提供器的全称(fully qualified)类名(小写)。因此,例如,一个TransportationProvider类可以定义如下:
public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transporationprovider");
如果这个内容提供器有子表,那么为每个子表也都定义CONTENT_URI常量。这些URIs应该全部拥有相同的权限(既然这用来识别内容提供器),只能通过它们的路径加以区分。例如:
content://com.example.codelab.transporationprovider/train
content://com.example.codelab.transporationprovider/air/domestic
content://com.example.codelab.transporationprovider/air/international
请查阅本文最后部分的Content URI Summary以对content: URIs有一个总体的了解。
· 定义内容提供器返回给客户端的列名。如果你正在使用一个底层数据库,这些列名通常和SQL数据库列名一致。同样还需要定义公共的静态字符串常量用来指定查询语句以及其它指令中的列。
确保包含一个名为"_id"(常量_ID)的整数列来返回记录的IDs。你应该有这个字段而不管有没有其它字段(比如URL),这个字段在所有的记录中是唯一的。如果你在使用SQLite数据库,这个_ID 字段应该是下面的类型:
INTEGER PRIMARY KEY AUTOINCREMENT
其中AUTOINCREMENT描述符是可选的。但是没有它,SQLite的ID数值字段会在列中已存在的最大数值的基础上增加到下一个数字。如果你删除了最后的行,那么下一个新加的行会和这个删除的行有相同的ID。AUTOINCREMENT可以避免这种情况,它让SQLite总是增加到下一个最大的值而不管有没有删除。
· 在文档中谨慎的描述每个列的数据类型。客户端需要这些信息来读取数据。
· 如果你正在处理一个新的数据类型,你必须定义一个新的MIME类型在你的ContentProvider.getType()实现里返回。这个类型部分依赖于提交给getType()的content: URI参数是否对这个请求限制了特定的记录。有一个MIME类型是给单个记录用的,另外一个给多记录用。 使用Uri 方法来帮助判断哪个是正在被请求的。下面是每个类型的一般格式:
² 对于单个记录: vnd.android.cursor.item/vnd.yourcompanyname.contenttype
比如,一个火车记录122的请求,URI如下
content://com.example.transportationprovider/trains/122
可能会返回这个MIME类型:
vnd.android.cursor.item/vnd.example.rail
² 对于多个记录: vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
比如, 一个所有火车记录的请求,URI如下
content://com.example.transportationprovider/trains
可能会返回这个MIME类型:
vnd.android.cursor.dir/vnd.example.rail
· 如果你想暴露过于庞大而无法放在表格里的字节数据-比如一个大的位图文件-这个给客户端暴露数据的字段事实上应该包含一个content: URI字符串。这个字段给了客户端数据访问接口。这个记录应该有另外的一个字段,名为"_data",列出了这个文件在设备上的准确路径。这个字段不能被客户端读取,而要通过ContentResolver。客户端将在这个包含URI的用户侧字段上调用ContentResolver.openInputStream() 方法。ContentResolver会请求那个记录的"_data"字段,而且因为它有比客户端更高的许可权,它应该能够直接访问那个文件并返回给客户端一个包装的文件读取接口。
自定义内容提供器的实现的一个例子,参见SDK附带的Notepad例程中的NodePadProvider 类。
声明内容提供器Declaring the content provider
为了让Android系统知道你开发的内容提供器,可以用在应用程序的AndroidManifest.xml文件中以<provider>元素声明它。未经声明的内容提供器对Android系统不可见。
名字属性是ContentProvider子类的全称名(fully qualified name)。权限属性是标识提供器的content: URI的权限认证部分。例如如果ContentProvider子类是AutoInfoProvider,那么<provider>元素可能如下:
<provider name="com.example.autos.AutoInfoProvider"
authorities="com.example.autos.autoinfoprovider"
. . . />
</provider>
请注意到这个权限属性忽略了content: URI的路径部分。例如,如果AutoInfoProvider为各种不同的汽车或制造商控制着各个子表,Note that the authorities attribute omits the path part of a content: URI. For example, if AutoInfoProvider controlled subtables for different types of autos or different manufacturers,
content://com.example.autos.autoinfoprovider/honda
content://com.example.autos.autoinfoprovider/gm/compact
content://com.example.autos.autoinfoprovider/gm/suv
这些路径将不会在manifest里声明。权限是用来识别提供器的,而不是路径;你的提供器能以任何你选择的方式来解释URI中的路径部分。
其它<provider>属性可以设置数据读写许可,提供可以显示给用户的图标和文本,启用或禁用这个提供器,等等。如果数据不需要在多个内容提供器的运行版本中同步则可以把multiprocess属性设置成"true"。这使得在每个客户进程中都有一个提供器实例被创建,而无需执行IPC调用。
这里回顾一下content URI的重要内容:
A. 标准前缀表明这个数据被一个内容提供器所控制。它不会被修改。
B. URI的权限部分;它标识这个内容提供器。对于第三方应用程序,这应该是一个全称类名(小写)以确保唯一性。权限在<provider>元素的权限属性中进行声明:
<provider name=".TransportationProvider"
authorities="com.example.transportationprovider"
. . . >
C. 用来判断请求数据类型的路径。这可以是0或多个段长。如果内容提供器只暴露了一种数据类型(比如,只有火车),这个分段可以没有。如果提供器暴露若干类型,包括子类型,那它可以是多个分段长-例如,提供"land/bus", "land/train", "sea/ship", 和"sea/submarine"这4个可能的值。
D. 被请求的特定记录的ID,如果有的话。这是被请求记录的_ID数值。如果这个请求不局限于单个记录, 这个分段和尾部的斜线会被忽略:
content://com.example.transportationprovider/trains
发表