Content Providers

http://docs.google.com/View?id=dcz49tvn_105cqq7tsdz

 

Content Providers

 

Content provider 存取数据并使它对其它应用程序可见. 它们是应用程序间共享数据的唯一方法; 没有其他的公有数据区域.

 

android内置了一些常用数据类型的content provider(音频,视频,图像,联系人信息, 等等). 你可以在android.provider包中看到它们. 你可以操作这些provider来查询你需要的数据(需要首先获取权限).

 

如果你希望让你的数据可以公用, 你有两种选择: 你可以创建你自己的content provider(一个ContentProvider子类)或者你可以为已存在的provider增加数据 -- 如果有一个content provider控制着你想共享的同样类型的数据并且你有权限读写它.

 

本文档是一个使用content provider的介绍. 在一个基本的讨论后, 它介绍了如何查询一个content provider, 如果修改content provider控制的数据以及如何创建你自己的content provider.

Content Provider Basics Content Provider基础

 

一个content provider如何储存它的数据取决于它的设计者. 但是所有的content provider实现了一个公用接口来查询和返回结果 -- 以及增加,改变和删除数据.

 

该接口是由客户间接调用的, 一般通过ContentResolver 对象. 你可以在Activity或其它应用程序组件的实现中使用getContentResolver()来获取一个ContentResolver.

ContentResolver cr = getContentResolver();

 

然后你就可以使用ContentResolver的方法来与你感兴趣的content provider进行交互.

 

当一个查询启动时, android系统找到目标content provider并确保它运行. 系统初始化所有的ContentProvider对象; 你不需要自己去做. 事实上, 你永远不会直接处理ContentProvider对象. 一般来说, 任何一种ContentProvider只有一个实例. 但是它可以和许多不同搞程序和进程中的ContentResolver对象进行交互. 进程间的交互由ContentResolver和ContentProvider来处理.

The data model 数据模型

 

Content provider将它们的数据在一个简单的表中暴露出来, 每一行是一条记录, 每一列是具有特定类型和意义的数据. 例如, 联系人信息的形式如下:

_IDNUMBERNUMBER_KEYLABELNAMETYPE
13(425) 555 6677425 555 6677Kirkland officeBully PulpitTYPE_WORK
44(212) 555-1234212 555 1234NY apartmentAlan VainTYPE_HOME
45(212) 555-6657212 555 6657Downtown officeAlan VainTYPE_MOBILE
53201.555.4433201 555 4433Love NestRex CarsTYPE_HOME

 

每条记录包含一个数字_ID域来唯一指定表中的一条记录. ID可以被用来匹配相关表格中的不同记录 -- 例如, 在一张表格中查询一个人的电话号码, 在另一张表格中查找这个人的照片.

 

一个查询返回一个Cursor对象, 该对象可以读取记录的每个域. 它有专门的方法来读取每种数据. 因此, 要读取一个域, 你必须知道它包含的数据类型.

URIs

 

每个content provider有一个公有URI(封装在一个Uri对象中), 该URI唯一指定了它的数据集. 一个控制多个数据集(多张表)的content provider为每张表暴露一个单独的URI. 所有的content  provider的URI以字符串"content://"起始.

 

如果你要定义一个content provider, 那么最好为它的URI定义一个常数. android为内置content provider定义了CONTENT_URI常数. 例如, 匹配电话号码的表格和匹配照片的表格的URI为:

 

android.provider.Contacts.Phones.CONTENT_URI
android.provider.Contacts.Photos.CONTENT_URI

类似的, 最近电话和日历事项的URI为:

 

android.provider.CallLog.Calls.CONTENT_URI
android.provider.Calendar.CONTENT_URI

 

URI常数在所有的和content provider打交道的场合都要使用. 每个ContentResolver方法都将URI作为它的第一个参数. 它标明了ContentResolver应该和哪个provider交互以及目标表格是哪一张.

Querying a Content Provider 查询一个Content Provider

要查询一个content provider, 你需要下列三个要素来

  • 标识provider的URI
  • 你希望接收的数据域的名称
  • 这些域的数据类型

 

如果你要查询一条特定记录, 你还需要这条记录的ID.

Making the query 查询

 

要查询一个content provider, 你可以使用 ContentResolver.query() 方法或者 Activity.managedQuery()方法. 两种方法都接收同样的参数, 都返回一个Cursor对象. 不同之处是: managedQuery()让activity来管理Cursor的生命周期. 一个托管Cursor处理所有的细节, 例如在activity暂停时卸载自身, 并在activity重启使重新查询. 你可以请求一个activity管理一个非托管的Cursor对象, 调用Activity.startManagingCursor().

 

query()或managedQuery()的第一个参数为provider URI -- 标识一个特定ContentProvider和数据集的CONTENT_URI常数.

 

要将查询限定在一条记录的话, 你可以将_ID值附在URI末尾, 例如, ID为23时:

content://. . . ./23

 

有一些helper方法, 例如ContentUris.withAppendedId() Uri.withAppendedPath() 来方便的为URI附加ID. 这两个都是静态方法, 返回增加了ID的Uri. 因此, 例如我们希望查找联系人数据库中的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则返回所有的列. 否则, 只返回列出的那些列. 所有的内置content provider为它们的列都定义了名字. 例如 android.provider.Contacts.Phones类为如前所述的电话表定义了每一列的名称 -- _ID, NUMBER, NUMBER_KEY, NAME等等.
  • 一个用来指示返回哪些行的filter, 按照SQL WHERE的语法格式(不含WHERE关键字本身). 传入null值则返回所有的行(除非URI将行限定为单条记录).
  • 选择参数.

  • 返回列的排序顺序, 按照SQL 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类实现了这些接口, 因此上面的代码示例可以使用该类名来引用这些常数.

What a query returns 查询的返回

 

一个查询返回0个或者多个数据库记录. 每个content provider的列名, 默认顺序以及数据类型各自不同. 但是每个provider都有一个_ID列, 该列为每个记录保存了一个唯一的数字ID. 每个provider还可以使用_COUNT来得到返回的记录条数; 该值在每一行中都相同.

Here is an example result set for the query in the previous section:

_ID_COUNTNAMENUMBER
443Alan Vain212 555 1234
133Bully Pulpit425 555 6677
533Rex Cars201 555 4433

 

得到的数据可以使用Cursor对象来查看, 该Cursor可以用来顺序和逆序遍历返回值集合. Cursor只能用来读数据. 要增加,修改和删除数据, 必须使用一个ContentResolver对象.

Reading retrieved data 读取数据

 

查询返回的Cursor对象提供了访问查询结果的功能. 如果你使用ID来查询一个特定的记录, 那么该Cursor只包含一条记录. 否则它有可能含有多个记录. (如果没有匹配项则为空.)你可以读取记录中的特定域, 但是你必须知道该域的数据类型, 因为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());

    
}
}

 

如果一个查询可以返回二进制数据, 例如图像或声音, 那么这些数据可以被直接输入表,或者该数据的表项为一个可以获得该数据的URI. 一般来说, 小量的数据(例如从20到50k字节)直接输入表格, 可以使用Cursor.getBlob()来读取. 它返回一个字节数组.

 

如果表项是一个content: URI, 那么你不能直接打开和读取该文件(权限问题会导致该操作失败), 而是需要调用ContentResolver.openInputStream()来获取一个InputStream对象, 你可以使用该对象来读取数据.

Modifying Data 修改数据

 

一个content provider管理的数据可以以下列方式修改:

  • 增加一条新的记录
  • 为已有记录增加新值
  • 批量更新已有记录
  • 删除记录

 

所有的数据修改使用ContentResolver的方法来完成. 有些content provider需要更加限制性的权限来写数据. 如果你没有写content provider的权限, 那么 ContentResolver方法将失败.

Adding records 增加记录

 

要为content provider增加一条记录, 首先在一个ContentValues对象中建立一个键-值对的映射, 每个键匹配content provider中一行的名字, 而值是新记录在这一样需要的值. 然后调用ContentResolver.insert()并将provider的URI和ContentValues映射传给它. 该方法返回新记录的完整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);

Adding new values 增加新值

 

一旦一条记录存在了, 你就可以为它增加新的信息, 或者修改已有信息. 例如, 上述例子的下一步将是增加联系人信息 -- 例如电话号码或者IM或者email地址.

 

最好的在联系人数据库中增加记录的方法是将表名附在URI后,然后使用这个URI来增加新的数据值. 每个联系人表有一个CONTENT_DIRECTORY常数来用作此用途. 下列代码延续上个例子, 并增加一个电话号码和一个email地址:


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, 然后调用 ContentResolver.openOutputStream(), 传入文件的URI. 这将使content provider将该数据存在一个文件并将该文件的路径放在记录的一个隐藏域中.

 

在这里, MediaStore content provider, 也就是提供图片,声音和视频数据的主要provider采用了一个特殊的惯例: query()或者managedQuery()用来获取二进制数据的元数据(例如照片的标题或者拍摄日期等)的URI被用来传给openInputStream()来获取这项数据. 类似的, insert()用来为MediaStore记录增加元数据的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 删除记录

 

 

要删除一个单独的记录, 调用ContentResolver.delete()并传入下述的URI.

要删除多行, 调用ContentResolver.delete()并传入需要删除的记录类型的URI(例如, android.provider.Contacts.People.CONTENT_URI) 和一个SQL WHERE从句来定义要删除的行. (注意: 如果要删除一个普通类型, 一定要包含一个有效的WHERE从句, 否则可能删除有用数据!)

Creating a Content Provider 创建Content Provider

要创建一个content provider, 你必须:

  • 建立一个用来存储数据的系统. 大部分content provider使用android的文件或者SQLite数据库来储存它们的数据, 但是你也可以用你想用的任何其它方式来存储数据. android提供了SQLiteOpenHelper类来帮助你创建一个数据库, 用SQLiteDatabase类来管理它.
  • 扩展ContentProvider类来提供数据的存取.
  • 在manifest文件中定义content provider.

 

Extending the ContentProvider class 扩展ContentProvider类

 

你需要定义一个ContentProvider的子类来将你的数据提供给别人. 理论上你需要定义6个方法:

query()
insert()
update()
delete()
getType()
onCreate()

 

 

query()方法必须返回一个Cursor对象, 该对象能够遍历被请求的数据. Cursor本身是一个接口, 但是android提供了许多已经定义好的Cursor对象供你使用. 例如, SQLiteCursor可以遍历SQLite数据库中存储的数据. 你可以使用SQLiteDatabase类的query()方法来取得Cursor对象. 有其它的Cursor实现 -- 例如MatrixCursor -- 用来读取不存在数据库中的数据.

由于这些ContentProvider方法可以被不同的进程和线程中的ContentResolver访问, 所以必须用考虑线程安全性.

 

当数据被修改时, 你有可能需要调用ContentResolver.notifyChange()来告诉监听器这一点.

 

除了定义这个子类之外, 你还需要一些其它的步骤来简化客户的操作:

  • 定义一个常量 Uri 叫做CONTENT_URI. 这是表示你的content provider处理的完整content: URI的常量.  你必须为该值定义一个唯一的字符串. 最好的方法是使用provider的完整的名称(小写). 例如TransportationProvider 的URI如下:

    public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transporationprovider");

    如果该provider还有子表, 那么为每个子表单独定义一个CONTENT_URI. 例如:

    content://com.example.codelab.transporationprovider/train
    content://com.example.codelab.transporationprovider/air/domestic
    content://com.example.codelab.transporationprovider/air/international

  • 定义content provider返回给客户的列名. 如果你使用一个数据库, 那么这些列名一般和SQL数据库的列名直接相同. 还要定义一些公有字符串常量, 以便用户在查询和其它指令中使用.

    保证包含一个叫做"_id"(使用常数_ID)列, 保存每条记录的ID. 无论你是否有另一个唯一的域, 你都应该有一个ID列. 如果你使用SQLite数据库, 那么_ID域应该为以下类型:

    INTEGER PRIMARY KEY AUTOINCREMENT

     AUTOINCREMENT为可选的. 但是如果没有它, SQLite会增加ID计数器使之成为已有最大ID的后一个. 如果你删掉最后一行, 那么下一个增加的行会具有和被删掉的行有同样的ID. AUTOINCREMENT可以避免这一点.

  • 仔细的为每列的数据类型编写文档, 客户需要这个信息来读取数据.

  • 如果你要处理一个新的数据类型, 那么你必须在你对ContentProvider.getType()中定义一个新的返回MIME类型. 该类型部分取决于提交给getType()的content:URI是否限制对特定记录的请求. 对单独记录有一种MIME类型, 而对多条记录有另一种记录类型. 使用Uri方法来帮助决定哪一种正被请求. 这里是每种类型的基本格式:

    • 对单独的记录:    vnd.android.cursor.item/vnd.yourcompanyname.contenttype

      例如, 一个对train记录122的请求:

      content://com.example.transportationprovider/trains/122

      可能返回这样的MIME类型:

      vnd.android.cursor.item/vnd.example.rail

    • 对多条记录:   vnd.android.cursor.dir/vnd.yourcompanyname.contenttype

      例如, 一个对所有train记录的请求:

      content://com.example.transportationprovider/trains

      可能返回这样的MIME类型:

      vnd.android.cursor.dir/vnd.example.rail

  • 如果你需要提供较大而不能直接放入表格的数据, 那么暴露给客户的数据域应该包含一个content: URI字符串. 这是给客户对数据文件访问的域. 该记录应该有另一个域, 称为"_data", 它列出了文件的实际存储位置. 该域不能被客户读取, 而是被ContentResolver读取. 客户会对URI域调用ContentResolver.openInputStream(). ContentResolver 将请求这条记录的_data域, 由于它的权限高于用户, 它应该能够直接访问文件并返回一个文件的读写wrapper给客户.

 

Declaring the content provider 声明content provider

 

要使android系统知道你开发的content provider的存在, 必须在AndroidManifest.xml文件中声明之. 否则它对系统不可见.

 

ContentProvider的name属性是它的全名. athority属性是content:URI的authority部分. 例如AutoInfoProvider的<provider>元素如下:


<provider name="com.example.autos.AutoInfoProvider"
          
authorities="com.example.autos.autoinfoprovider"
          . . . 
/>
</provider>

注意authorities属性忽略了content:URI的路径部分. 例如, 如果AudioInfoProvider控制不同类型的汽车和不同生产商的子表,

content://com.example.autos.autoinfoprovider/honda
content://com.example.autos.autoinfoprovider/gm/compact
content://com.example.autos.autoinfoprovider/gm/suv

这些路径不会在manifest中声明. 是authority,而不是path来标识provider. 你的provider可以按你想要的方法解释uri的路径部分.

 

其它的<provider>属性可以设置读写数据的权限, 提供一个图标和一段文本来显示给用户, 使provider有效和无效等. 如果数据不需要在多个运行的不同版本的content provider之间同步, 那么将multiprocess属性设置为"true".这将允许在每个客户进程中创建一个provider的实例, 消除了进行IPC的必要性.

Content URI Summary Content URI总结

这里我们复习一下content: URI的各个部分:

 

 


 

  1. 标准前缀, 指示该数据是由一个content provider控制的.
  2. URI的authority部分; 用来标识一个content provider. 对于第三方的应用程序, 它应该是完整的类名(小写)以便保证唯一性. authority需要在<provider>的authorities属性中声明:


    <provider name=".TransportationProvider"
              
    authorities="com.example.transportationprovider"
              . . .  
    >

  3. 路径部分; content provider用它来决定什么数据正在被请求. 它可以为空. 如果content provider只暴露一种数据(例如, 只有trains), 那么可以为空. 如果需要暴露多种, 那么可能会有好几段(例如"land/bus", "land/train", "sea/ship", "sea/submarine"等.
  4. 被请求的特定记录的ID. 如果请求不是限制在某一条记录上, 该段以及斜杠都被省略.
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值