Android开发指南中文版(七)Content Providers

Content Providers

Content provider存储和检索数据并使得所有应用程序可以轻松的使用数据。这是应用程序之间分享数据的唯一途径;没有普通的存储区使得所有Android包可以通过。

Android使用很多content provider来运送普通数据类型(audio, video, images,个人通讯信息等)。你可从android.provider包中找到它们。你可以通过这些provider来查询它们包含的数据(但是,必须获得读取这些数据的合适的权限)。

如果你想公开自己的数据,有两种选择:你可以创建自己的content provider(一个ContentProvider的子类)或着你可以将自己的数据添加到一个存在的provider---该provider控制相同类型的数据并且你有权限来使用。

该文档介绍如何使用content provider,在简要的讨论后将探究如何查询content provider,如何修改由provider控制的数据以及如何创建一个自己的content provider。

Content Provider基本要素

一个content provider底层如何存储数据取决于它的设计。但是所有content provider实现了一套公共的接口来查询和返回结果---添加,修改和删除数据也是如此。

Content provider是一个用户间接使用的接口,大多数通过ContentResolver使用。你可以调用Activity或其他组件中实现的getContentResolver()方法来获得一个ContentResolver。

ContentResolver cr = getContentResolver();

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

当一个查询初始化时,Android系统识别查询的目标content provider并确保它被启动并运行。系统会实例化所拥有ContentProvider对象;你不需要自己做这些。事实上你永远不会直接处理ContentProvider对象。通常,每种数据类型的ContentProvider对象只有一个实例。但是可以与不同程序或进程中的多个ContentProvider交互。进程之间的相互作用是通过ContentProvider和ContentResolver类处理。

数据模型:

Content provider以数据库模式的简单的表格形式公布它的数据,表格的每一行是一条记录,每一列代表一种类型或意义的数据。比如说,一个人的信息以及它的电话号码可以如下表示:

_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对象,它可以从一条记录移动到另一条记录以及从一列移动到另一列来读取每个字段的内容。它有专门的方法来读取不同类型的数据。所以在读一个字段时,你必须知道该字段包含的数据的类型。

URI

每个content provider公开一个公共的URI(由Uri对象封装),该URI可以唯一的标识它的数据集合。一个控制多个数据集的content provider为每一个数据集合公开一个URI。所有provider的URI都以"content://"字符串开头。这个content: scheme识别被content provider控制的数据。

如果你定义一个content provider,也最好为它的URI定义一个常量来简化客户端代码。Android为所有来自平台的provider声明了CONTENT_URI常量。比如说,匹配电话号码和用户的表格的URI以及存储用户图片的URI(都是由Contacts content provider控制)如下:

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

URI常量用于所有与content provider的交互。每个ContentResolver的方法将URI作为第一个参数。它是标识哪个provider可以对话以及provider的哪个表格是目标。

查询 Content Provider

查询一个content provider你需要3个信息:

  • 标识provider的URI
  • 要检索的数据字段的名称
  • 这些字段的数据类型

如果你要查询一个指定的记录,你需要这个记录的ID。

执行查询:

你可以使用ContentResolver.query()或Activity.mangedQuery()方法来查询一个content provider。两个方法都有相同的形参且都返回一个Cursor对象。然而mangedQuery()方法会导致activity控制Cursor的生命周期。一个被控制的Cursor处理所有细节,如当activity pause时卸载自己,当activity restart时重新查询。你可以调用Activity.startManagingCursor()让activity来控制一个未被控制的Cursor。

query()和mangedQuery()的第一个参数是provider的URI----标识特定ContentProvider和数据集合的CONTENT_URI 常量。

为了将查询限制在一条记录,你可以将哪个记录的_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()和mangedQuery()的其它参数来限定查询的细节,它们是:

  • 应该被return的数据列的名称。null值将返回所有列。否则,只有被列举了名称的列才被返回。所有来自于平台的content provider都为其列声明了一个常量。比如:android.provider.Contacts.Phones 类为电话表的类的名称声明的常量有:_ID, NUMBER, NUMBER_KEY, NAME, 等。
  • 一个决定哪行被返回的filter。它是用SQL的WHERE从句的格式(不包含WHERE)。null值将返回所有的行(除非URI限制只查询一条记录)。
  • 可供选择的形参
  • 返回的行的排列顺序,它是用SQL的ORDER BY从句的格式(不包含ORDER BY)。null将按照其在表中的默认顺序返回。

下面是一个查询联系名列表和主要电话号码的例子:

import android.provider.Contacts.People;
import android.database.Cursor;
// 在一个数组中指定那一列要返回 
String[] projection = new String[] {
                             People._ID,
                             People._COUNT,
                             People.NAME,
                             People.NUMBER
                          };
// 获得Contacts content provider中的People表格的base URI.
Uri contacts =  People.CONTENT_URI;
// 执行查询. 
Cursor managedCursor = managedQuery(contacts,
                         projection, // 返回哪一列 
                         null,       // 返回哪一行 (null:所有行)
                         null,       // 选择参数 (none)
                         // 以name递增的顺序对返回的结果集排序
                         People.NAME + " ASC");

 

这个查询从Contacts content provider 的People表返回数据。它获得联系人的name, 主要 phone number和 唯一的record ID,_COUNT。

在不同的接口声明了列名的常量—在BaseColumns中的_ID和_COUNT,在PeopleColumns中的NAME和在PhoneColumns中的NUMBER。Contacts.People类实现了上面的每个接口,所以上面代码中可以使用所有这些名字。

查询返回什么:

查询返回零个或多个数据库字段的集合。每个provider都有_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对象公开。它可以在结果集上向前或向后重复。你只能用这个对象来读取数据。

读取检索数据:

查询返回的Cursor对象提供了访问返回的数据集的方法。如果你有一个查询来获得一个由ID指定的记录,返回的集合将只包含一个值。其他情况下将包含多个值。(如果没有匹配,它也可以是空的。)你可以从记录中的特定的字段读取数据,但是你必须知道则这个字段的数据类型,因为Cursor对象用不同的方法来读取不同数据类型---比如getString(),getInt()和getFloat()。(然而,对大多数类型,如果你调用读取string的方法,Cursor对象会为你返回这个数据的字符串表示。)Cursor允许你通过列的索引获得列名或者通过列名获得索引。

下面是上例中从查询结果读取name和phone number的例子:

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对象。

修改数据

Content provider的数据可以被修改:

  • 添加一个新的记录
  • 给存在的记录添加一个新的值
  • 批量修改存在的记录
  • 删除记录

所有的修改操作都是通过ContentResolver的方法完成。比起读数据,一些content provider需要更为严格的权限来写数据。如果你没有权限来写入content provider,ContentResolver的方法将失败。

添加记录:

为了给一个content provider添加一个新的记录,首先用一个ContentValues对象建立一个含有键值对的map,它的每个key匹配在content provider中的一个列的名字,每个value是新的记录在该列的值。然后调用ContentResolver.insert()并将provider的URI和这个ContentValues map传递给它。这个方法返回新的记录的全部URI----传递的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或者是e-mail地址。Contact数据库里添加记录的最好的方法,就是将要添加新数据的表的名字附加在URI上,然后用这个修正的URI来增加新的数据值。 每一个联系人表为了这个目的将给出一个作为CONTENT_DIRECTORY 常量的名字,继续上面的例子,为新创建的记录添加一个电话号码和e-mail地址。

Uri phoneUri = null;  
Uri emailUri = null;  
// 为Abraham Lincoln添加一个电话号码.  先从由insert()返回的新纪录的URI开始,它以新纪录的_ID结尾   
//所以我们不需要自己添加ID。   
// Then append the designation for the phone table to this URI,   
// 并用返回的 URI 来插入一个电话号码   
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);  
// 用相同的方法添加 email 地址.   
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()(形参有一个byte数组)将少量的小的二进制数据放入表中。如一个图标式的图像或一小段音频可以用此方式。然而如果你有大量的二进制数据要添加,比如一幅照片或者一首完整的歌,应在表格中放置一个该数据的content:URI并调用ContentResolver.openOutputStream()(形参为文件的URI)。(这将使content provider将数据存入文件并在记录的一个隐藏字段记录文件的路径)。

在这一点,MediaStore content provider(分配image, audio和 video 数据的主要provider)利用了一个特殊的协议:与query()和managequery()一起使用来获得二进制数据的元信息(比如,照片的标题或者拍摄的日期等等)的URI,与openInputStream()一起使用就可以来获得它自身的数据。同样的,与insert()一起使用来将元信息存入MediaStore记录的URI,也被用来与openOutputStream()一起来放置那里的二进制数据。下面的程序说明了这个过程:

import android.provider.MediaStore.Images.Media;  
import android.content.ContentValues;  
import java.io.OutputStream;  
// 在 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");  
//添加一个新的记录不是用bitmap,而是用刚才设置的值.   
// insert() 返回新纪录的 URI.   
Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, values);  
// 现在获得一个该记录的文件的句柄,将数据存入.   
// 这里, sourceBitmap 是一个 Bitmap 对象,它代表了要存如数据库的文件.   
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);  
}  
批量修改记录:

要批量修改一组记录的数据(如将所有字段中的”NY ”修改为”New York”),调用函数ContentResolver.update(),传入列以及要修改的值。

删除记录:

要删除一条记录,调用ContentResolver.delete(),并传入指定行的Uri

要删除多行,调用ContentResolver.delete()并传入要删除的这一类记录的Uri(比如android.provider.Contacts.People.CONTENT_URI)和一个SQL WHERE形式的语句来指明哪行要删除。

创建一个Content Provider

要创建一个content provider你应该:

  • 建立一个存储数据的系统。多数的content provider用Android的文件存储方法或SQlite数据库来存储数据,但是你可以用任何你想用的方法存储。Android提供了SQLiteOpenHelper类来帮助你创建一个数据库以及SQLiteDatabase来管理它。
  • 继承ContentProvider类来提供对数据的访问
  • 在manifest文件(AndroidManifest.xml)中为你的应用程序声明这个content provider
继承ContentProvider

要实现6个ContentProvider类中声明的抽象方法:query() 、insert()  、update()  、delete()  、getType()  、onCreate() 。

query() 方法必须返回一个可以遍历请求的数据的Cursor对象。Cursor本身是个接口,但是Android提供了一些你可使用的现成的Cursor对象。比如说,SQLiteCursor可以在SQLite数据库中存储的数据上迭代。你可以通过SQLiteDatabase类的query()方法获得该Cursor。还有其他的Cursor实现—如为没有存储在数据库中的数据准备的MatrixCursor。

因为这些ContentProvider方法可以在不同的进程和线程中使用各种ContentResolver对象调用,所以它们必须被实现为线程安全的。

当要数据改变时,你也许同样会调用ContentResolver.notifyChange()来通知listener。

在定义子类之后,还有其它的步骤:

  • 定义一个public static final Uri CONTENT_URI。这是一个代表你的content provider持有的全content: URI的字符串,你必须给其定义一个唯一的值。最好的方法是使用content provider的fully-qualified类名(小写)。比如,一个TransportationProvider类的URI可以这样定义:
    public static final Uri CONTENT_URI =   
                    Uri.parse("content://com.example.codelab.transportationprovider");  
    
  • 如果这个provider还有子表,也要为每个子表定义CONTENT_URI常量。这些常量必须有相同的authority,只有path不同,比如:

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

  • 定义content provider将要返回给用户的列的名字。如果你使用了底层的数据库,这些列名通常与SQL数据库列名相同。同样是声明为public static字符串常量,用户可以使用它们来指明要查询的列。
  • 确保要提供一个整型的列,列名为"_id"(常量为_ID)作为记录的ID。必须有这个字段。如果你使用SQLite数据库,_ID应该是下面的类型:

    INTEGER PRIMARY KEY AUTOINCREMENT

    AUTOINCREMENT是可选的,但是没有它,SQLIte增加一个ID计数器字段来记录最大记录数的下一个值。如果你删除了最后的一行,添加的下一行将会与删除的一行有相同的ID。AUTOINCREMENT 可以让SQLite来处理ID的增长以避免上述情况的发生。

  • 备注各列的每一种数据类型,用户需要它来读取数据。
  • 如果你要处理一个新的数据类型,你必须声明一个新的MIME类型来作为你实现ContentProvider.getType()的返回。这个类型部分取决于提交给getType()的content:URI是否限制对具体记录的请求。对单个记录有一种形式,对多个记录又有另一种形式。用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

  • 如果你要公布因太大而不能放入表里的二进制数据,公布(expose)给用户的数据的的字段应该包含content:URI字符串。这是让用户可以利用数据文件的字段。这个记录也应该有另一个字段,名为_data"来列出设备上那个文件的路径。这个字段不打算由用户读取,而是供ContentReslover使用。用户只要在user-facing 的字段调用
  • ContentResolver.openInputStream()方法。ContentResolver会请求"_data"字段获得那条记录。因为它有比用户高的权限,所以它可以直接访问文件并为用户返回读取的结果。

声明content provider

要让Android系统知道你开发的content provider,你应该在AndroidManifest.xml文件中用 <provider> 元素声明。未声明的content provider不可用。

       <provider>元素的name属性是ContentProvider子类的全名。authorities 属性是标识provider的URI中的authority部分。比如,ContentProvider子类如果是AutoInfoProvider,<provider> 元素或许如下:

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

注意authorities 属性省略了content:URI的path部分。比如说,如果AutoInfoProvider控制着不同类型汽车或不同生产厂商的表格:

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

它们的path不会在manifest中声明。authority 标识这个provider,而不是path;你的provider可以以你选的的任何方式来解释(interpret)URI的path部分。

其他的< provider >属性可以设置读写数据的权限,为其提供一个图标或者文本来显示给用户,使provider可用或不可用等等。如果数据不需要在多个运行版本的content provider同步,将multiprocess 属性设置为"true"。这会允许在每个客户端进程创建一个provider的实例,消除了执行IPC的需要。(IPC:进程间通信)

Content URI 总结:

这里是一个content URI的重要部分的概括:

    content_uri

A. 标准的前缀,指示数据由content provider控制。它永远不会被修改

B. URI的authority 部分,它标识这个content provider。对于第三方应用程序,这一部分应该是一个fully-qualified类名(小写)来确保唯一。Authority在< provider >元素的authorities 属性声明。

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

C.The path that the content provider uses to determine what kind of data is being requested. This can be zero or more segments long. If the content provider exposes only one type of data (only trains, for example), it can be absent. If the provider exposes several types, including subtypes, it can be several segments long — for example, "land/bus", "land/train", "sea/ship", and "sea/submarine" to give four possibilities.

D.The ID of the specific record being requested, if any. This is the _ID value of the requested record. If the request is not limited to a single record, this segment and the trailing slash are omitted:

   content://com.example.transportationprovider/trains

     

posted on 2011-12-31 17:53  蓝风笨笨  阅读(826)  评论(0编辑  收藏  举报

导航