Content Providers
Content providers存储与获取数据,并且使所有的应用都可以访问他。这是唯一的跨进程共享数据的方法,这里没有所有 Android包可以访问的通用数据区。
Android为一些常见的数据类型 (音频,视频,图像,个人联系信息等等 )提供了一组 content provider 。你可以在 android.provider包下看到列出的他们中的一些。你可以查询他们拥有的数据 (然而,有些 provider,你必须要拥有一定权限才可以 )。
如果你想让自己的数据公有化,你有两个选择:你可以创建你自己的 content provider ( 一个 content provider 的子类 )或者你可将你自己的数据添加到一个已存在的 content provider中去 (如果有一个 content provider 控制与你的相同的数据类型且你对其有写权限 )。
一个 content provider如何存储他的数据完全依赖于他的设计。但是所有的 content provider都实现一个通用的接口用以查询数据和返回结果,同时还有添加,修改,删除。
他是一个客户端可以间接使用的接口,最为常见的是通过 ContentResolver对象。你可以在一个 Activity的实现中或者别的组件中通过调用 getContentResolver()来获取一个 ContentResolver。
ContentResolver cr = getContentResolver();
你可以使用 ConentResolver的方法来与你感兴趣的任何 Content Provider来交互。
当一个查询初始化了, Android系统识别该查询的目标 content provider,并确保他已经运行起了。系统初始化所有的 ContentProvider对象,你不用自己去管这个。实际上,你永远不会直接处理 ContentProvider 。典型的,每个 ContentProvider只有一个实例存在系统中。但是他可以和不同的进程与应用的 ContentResolver通信。这种跨进程的交互在 ContentResolver与 ContentProvider类中处理。
ContentProvider根据数据库模型以一个简单的表的形式暴露他的数据,每一行代表一条记录,每一列是一个特殊类型的数据有他特别的意思。例如,人和他们的电话号话的信息可以像以下表来展露:
_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用来唯一识别表中的一条记录。 ID可以用来在相关的表中配置记录。例如,在一个表中查找一个人的电话号码,在另一个表中查到他的图片。
一个查询返回一个 Cursor对象,他可以从一条记录移动到另一条记录,也可以从一列移动到另一列来读取每一个域中的数据。他对每一种数据类型的读取都有其相应的方法。所以,读一个域的数据,你必须知道他包含的数据的类型。
每一个 Content Provider暴露一个公共的 URI(一个 Uri的对象 ),他用以唯一识别他的数据集。一个 content provider控制多个数据集(多个表),每个数据集各有一个 URI。所有 provider的 URI都以 ”content://”开始。 content:标识这个数据被 content provider控制的。
如果你定义一个 Content Provider,为 URI定义一个常量是一个好主意,他可以使客户端代码简单化也可以使将来维护清晰。来自平台的的 Content Provider的 URI都定义常量为 CONTENT_URI。例如:一个匹配电话到人和一个图片到人的表的 URI如下:
android.provider.Contacts.Phones.CONTENT_URI android.provider.Contacts.Photos.CONTENT_URI
这些 URI在所有的与 Content Provider交互的地方使用。所有的 ContentResolver方法都将 URI作为他的第一个参数。他用以唯一识别 ContentResolver应该与哪个 Content Provider交流,以及这个 Provider的哪一个表才是他的目标。
查询一个 content provider你需要三块信息:
URI用以识别哪个 Content Provider.
你想获取的数据域的名称。
这些域的数据类型
如果你要查询某一特定记录,你还需要那条记录的 ID。
查询一个 Content Provider,你可以使用 ContentResolver.query()方法,或者使用 Activity.managedQuery()方法。两个方法都采用同一组参数,都返回一个 Cursor对象。然而, managedQuery()使 Activity管理 Cursor的生命周期。一个托管的 Cursor处理他所有的变化,例如,当 activity暂停时上传他自己,当 activity重新开始时重新查询他自己。你可以通过 Activity.startManagedCursor()要求 Activity去管理一个没有被管理的对象。
两个方法 query()与 managedQuery()的第一个参数都是 Provider的 URI。 CONTENT_URI常量识别一个特殊的 ContentProvider和数据集。
约束只对一条记录的查询,你可以为那条记录的 URI添加上他的 _ID值,即是,放一个匹配的字符 ID作为 URI的最后一个部分。例如 ,如果 ID是 23,他的 URI应该是:
content://. . . ./23
这里有一些比较有用的方法,尤其是
ContentUris.withAppendedId()
和
Uri.withAppendedPath(),
他们使添加一个
ID
到
URI
简单化。两个静态方法都返回一个添加了
ID
的
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.Phone类定义了 phone表中的所有的列名为常量,如前图表所示。 _ID,NUMBER,NUMBER_KEY,NAME等等。
一个过滤器定义哪些行应该返回。格式化为一个 SQL的 where语句 (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");
这个查询从 Contacts的 content provider中获取 People表中的数据。他获取每一个联系人的姓名,主要电话号码,唯一的记录 ID。 他同时以每条记录的 _Count域返回记录的条数。
这些列名被定义到各种接口中。 _ID和 _COUNT在 BaseColumns中, NAME在 PeopleColumns中, NUMBER在 PhoneColumns中。 Contacts.People类实现了这些每一个接口,所以我们可以在以上的代码中仅使用类名即可以引用他。
一个查询返回 0条或者多条数据记录。列名,默认的排序,以及他们的数据类型被每一个 Content Provider所指定。但是每个 Provider都有一个 _ID列,他为每一条记录持有一个唯一的数字的 ID。每一个 provider可以通过 _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的对象。
Cursor对象是通过查询返回 ,提供了一个访问结果的记录集的途径。如果你是查询一个特殊 ID的记录,那个记录集只包含一个值。否则他可能包含多个值。 (如果没有匹配的,他也可以是空的。 ) 你可以读取记录中的特定字段的值,但是你必须知道这个字段的值类型,因为 Cursor对象针对每一种类型有一个特别的方法,比如: getString(),getInt(),getFloat() ( 然而,对于绝大多数类型,你可以使用读取字符串的方式来读取, Cursor对象会将该数据以字符串的形式返回给你 )。 Cursor要求你请求索引列的列名或者索引列名的索引列号。
下列片断演示了从之前的查询中读取名字与电话号码 :
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()来读取。他返回一个 byte的数组。
如果表输入的是一个 content:的 URI, 你不应该试图直接打开或者读取这个文件 (比如:权限问题有可能使其失败 )。你应该调用 ContentResolover.openInputStream()来获取一个 InputStream对象,你可以用以读取数据。
Content proivder持有的数据可以通过以下方式被修改:
添加新记录
添加新值到已存在的记录中去
批量更新已存在的记录
删除记录
所有 的数据的修改都通过 ContentResolver的方法来完成。一些 content provider需要比读取更多一些的权限来写数据。如果你没有权限对一个 content provider写数据,那么 ContentResolver的方法会失败。
添加一条新记录到一个 content provider,每一件事情是用 ContentValues对象创建一组键值对的映射,每个键匹配一个 content provider的列名,值是为新记录的那一列准备的值。传递 provider的 URI和 ContentValues的映射给 ContentResolver.insert()方法。这个方法返回新记录的全 URI,即,这个 provider的 URI添加了一个新记录的 ID。然后你可以使用这个 URI来查询,并且获得一个新记录的 Cursor,然后你可以对其作进一步的修改。这里有一个例子:
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);
一旦一个记录存在了,你可以为其添加新的信息,也可以修改其已存在的信息。例如,上例的下一步,你可以添加联系信息,像电话号码,或者 IM,或者 email地址,到这个新的条目。
在 Contacts数据库中添加一条记录的最好的方法是添加一个新数据进行的表名到这个记录的 URI,然后使用这个修正后的 URI添加新的数据值。为了这个目的每一个 Contacts表以 CONTENT_DIRECTORY常量暴露一个名字。以下代码继续之前的例子为刚刚创建的记录添加一个电话号码和一个 e-mail地址。
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, "[email protected]"); values.put(People.ContactMethods.TYPE, People.ContactMethods.TYPE_HOME); getContentResolver().insert(emailUri, values);
你可以通过调用 ContentValues.put()使用一组 byte数组将少量的二进制数据放入一个表中。比如,他可以是一个小的图片,或者一个小的音频片段。然而,如果你有一个大量的二进制数据需要添加,比如一张照片,或者一首完整的歌,放一个 content:的 URI到表中,并且和文件的 URI一起调用 ContentResolver.openOutputStream()。 (他促使 content provider在一个字段中存储数据并在记录的一个隐含字段中记录文件的路径。 )
在这点上, MediaStore content provider,主要分配图像,音频,视频的 provider,遵守一个特定的公约:相同的 URI用于 query()和 managedQuery()去获取二进制数据的元数据(例如照片的标题和拍照的时间),用 openInputStream()来获取数据本身。类似的,相同的 URI用以 insert()将元信息存入 MediaStore记录, 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);
}
批量更新一组记录,调用 ContentResolver.update()方法来改变。
如果删除单独一条记录,调用 ContentResolver.delete()和特定行的 URI。
删除多条记录,调用 ContentResolver.delete(),提供一个要删除数据类型的 URI和一个 SQL的 where 语句来定义哪些行应该被删。
要创建一个 Content Provider,你必须:
设置一个存储数据的系统。大多数 content provider使用 Android的存储方式或者 SQLite的数据库来存储他们的数据,但是你可以以你想要的任何方式来存储数据。 Android提供 SQLiteOpenHelper类来帮助你创建一个数据库以及 SQLiteDatabase来管理他。
扩展 ContentProvider类以提供对数据的访问。
在你的应用的 manifest文件中声明一个 Content Provider。
你定义一个 ContentProvider的子类使用 ContentResolver期望的公约和 Cursor对象来将你的数据暴露给其它组件或者应用。原则上,这意味着要实现 ContentProvider定义的六个抽象方法:
query()
insert()
update()
delete()
getType()
onCreate()
query()方法必须返回一个 Cursor对象,可以枚举遍历请求的数据。 Cursor他本身是一个接口,但是 Android提供了一些准备好了的 Cursor对象供你使用。例如: SQLiteCursor可以枚举存储在 SQLite数据库中的数据。你调用任何一个 SQLiteDatabase类的 query()方法可以获得一个 Cursor对象。这里还有一些别的 Cursor的实现,比如 MatrixCursor,数据没有存在一个数据库中。
由于这些 ContentProvider方法的调用可能来自己不同进程的不同线程的 ContentResolvoer对象,它们必须设计为线程安全的。
当有数据被修改时你可能会调用 ContentResolver.notifyChange()来通知你的监听者。
除了定义子类本身,下面还有其它步骤可以简化客户端的工作以及增该类的可访问性:
· 定义一个 public static final 的 URI ,命名为 CONTENT_URI。这是一个字符串,他表示为你的 content provider处理的完整的 content:的 URI。你必须为此定义一个唯一的字符串。最好的解决办法是使用 content provider(由小写)的全限定类名。例如 ,一个 TransportationProvider类可以如下定义:
public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transportationprovider");
如果 provider 还有子表,也应该为每一个子表各定义一个 CONTENT_URI 。这些 URI 都应该有相同的权限。只通过他们的路径进行区别。例如:
content://com.example.codelab.transportationprovider/train content://com.example.codelab.transportationprovider/air/domestic content://com.example.codelab.transportationprovider/air/international
· 定义 content provider将要返回给客户端的列的列名。如果你使用一个隐含的数据库,这些列名通常与数据库中的列表相同。也定义为 public static的字符串常量,客户端可以用以指定在查询或者别的指令中使用的列。
确保包含一个记录 ID 的列名为 _id 的列。(其静态常量为 _ID ),不管你有没有其它字段,你应该有这个字段,且所有记录间都是唯一的。如果你使用 SQLite 数据库, _ID 字段应该是如下类型:
INTEGER PRIMARY KEY AUTOINCREMENT
描述符 AUTOINCREMENT 是可选的。但是,如果没有它,一个 ID 的 SQLite 递增计数器字段上方的最大的下一个数列中的现有人数。但是如果你删除掉最后一行,下一个添加行的 ID 会与删除掉的行的 ID 值一样。AUTOINCREMENT
避免了通过
SQLite
增加到下一个最大值,不论是删除与否。
· 仔细记录每个列的数据的类型。客户需要这些信息来读取数据。
· 如果处理一个新的数据类型,你必须定义一个新的 MIME类型,在你的 ContentProvider.getType()中返回。这个类型部分依赖于 content:的 URI是否提交到 getType()限制到一个特殊记录的请求。这里有一个针对单一记录的 MIME类型的格式,另一个是针对多条记录的。使用 Uri方法有助于判断请求的是什么。下面是每个类型的普遍的格式 :
对一条单独的记录:
vnd.android.cursor.item/vnd.yourcompanyname.contenttype
例如一个对 train 的 122 的记录的请求,像这样的 URI :
content://com.example.transportationprovider/trains/122
可能返回这样的 MIME 类型:
vnd.android.cursor.item/vnd.example.rail
对于多条记录:
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
假如,一个对所有 train 记录的请求,像如下的 URI :
content://com.example.transportationprovider/trains
可能返回如下 MIME 类型:
vnd.android.cursor.dir/vnd.example.rail
· 如果暴露的二进制数据太大而不能放在这个表中,比如一个很大的位图文件,那个字段暴露给客户端的应该是一个 content:的 URI的字符串。这个字段让用户可以访问数据文件。该记录应该还有另一个字段 _Data,列出扩展文件在设备中的路径。这个字段不许客户端读取,但是可以被 ContentResolver访问。客户端将针对 用户面对的字段从那项中提出的 URI调用 ContentResolver.openInputStream()。 ContentResolver将要为这条记录请求 _data字段,因为他拥有比客户端更高的权限,他应该可以直接访问文件并且返回一个文件的可读的 wrapper给客户端。
让 Android系统知道你开发的 content provider, 在应用的 AndroidManifest.xml文件中使用一个
Name属性是 ContentProvider子类的完全保留名称。 Authorities
属性识别这个
Proivder
的
Uri
的权限部分。例如,一个
ContentProvider
的子类是
AutoInfoProvider,
的属性可能像这样:
注意 authorities 属性省略掉了路径中的 content: 部分。例如,如果 AutoInfoProvider 为不同类型的 auto 和不同类型的生产商控制着一些子表,
content://com.example.autos.autoinfoprovider/honda content://com.example.autos.autoinfoprovider/gm/compact content://com.example.autos.autoinfoprovider/gm/suv
这些路径不需要在 manifest中声明。 Authority是一个 Provider的一个标识,而不是一个路径,你的 Provider你可以选择以任何方式解释 URI的路径部分。
别的
这里有一个对 Content URI的重要部分的组成的概述。
A. A 标准的前缀,指定数据是由一个 content provider控制。他永远不会改变。
B. B URI的权限部分,他识别这个 content provider。对于一个三方的应用,他应该是一个完整的限定了的类名 (都转为小写 )去确保其唯一。权限在
C. C 路径: Content provider 用以决定哪种类型的数据被使用。它可以是 0个或者多个段。如果 content provider只暴露了一种数据类型 (例如,只有 trains),那么他可以缺失。如果 content provider暴露了多个类型,包括子类型,他可以有多个段,假如: ”land/bus”,”land/train”,”sea/ship”,”sea/submarine”。
D. D 如果 ID给定,则请求的是指定的记录。这是请求的记录的 _ID值。如果请求没有限制到一个单独的记录,这个段与斜杠应该省去。
content://com.example.transportationprovider/trains