[Pro.android.3][读书笔记]Android 04 理解内容提供者 连载

来源:互联网 发布:mac如何切换输入法 编辑:程序博客网 时间:2024/06/05 08:22

转自 http://blog.163.com/wangli_601/blog/static/122950173201162492247365/ 

 

Android使用一个叫做内容提供者(content providers)的概念用来把数据抽象成服务。内容提供者的这种思想使数据库看起来像一个表述性状态转移(REST-Representational State Transfer)的数据提供者,例如网站。在这种意义上来说,内容提供者是数据的包装。在Android设备中的SQLite数据库就是一个数据源的例子,你可以把它封装成为内容提供者。

  为了能够从内容提供这中获得或者保存数据,你需要使用一组URI。假如你需要从内容提供者封装的book数据库中检索书籍信息,你需要使用这样的URI:content://com.android.book.BookProvider/books

 而想要从book数据库中检索特殊的书籍信息(book 23),你需要使用这样的URI:

 content://com.android.book.BookProvider/books/23

 通过本章你将了解到URI是如何向底层可访问的数据库结构转化的。任何设备上的应用程序都可以使用URI访问和操作数据。由此推论,内容提供者在应用程序间的数据的共享操作中扮演着重要的角色。

 严格的说,尽管,内容提供者是一种封装机制,而不是数据的访问机制。你需要一个实际的数据访问如SQLite或通过网络机制访问底层数据源。因此,内容提供者只是在应用程序间共享数据的一种抽象。对于内部数据,应用程序可以使用任何的数据存储/访问机制,一下是一些合适的方式:

Preferences:可以存储键值对的数据。

Files:文件存储

SQLite:SQLite数据库,每个应用程序中都能创建一个私有的数据库。

Network:通过网络检索或者存储数据。

 

查看Android内建的内容提供者

Android有一些内建的内容提供者,这些内容提供者在SDK文档的android.provider Java包中都有介绍。你可以使用如下地址浏览这些内容提供者:http://developer.android.com/reference/android/provider/package-summary.html

这里是一些文档中的内容提供者的列表

Browser

CallLog

Contacts

     People

     Phones

     Photos

     Groups

MediaStore

      Audio

      Albums

      Artists

      Genres

      Playlists

Images

      Thumbnails

Video

Settings

处于顶级的表示数据库,顶级一下的项是表。所以Browser,CallLog,Contacts,MediaStore和Settings都是独立的数据库的封装。这些SQLite数据库都是以.db为后缀并且只允许在该应用程序包中使用。任何需要在该应用程序包以外的访问,都需要通过内容提供者实现。

 

在模拟器和可用设备中查看数据库

因为在Android中很多内容提供者使用的是SQLite数据库(http://www.sqlite.org),你能使用Android提供的工具和SQLite去检查数据库。大多数工具存放在\android-sdk-intall-Directory\tools目录下,其他的工具在\android-sdk\install-directory\platform-tools目录下。

Android使用的命令行工具叫作Android Debug Bridge(ADB),位于platform-tools\adb.exe。

adb在Android工具包中是一个很特殊的工具,许多其他的的工具通过它与设备连接。但是,你必须保证当前正在运行模拟器或者是一个已连接adb的Android设备。你可以通过下列命令判断是否有模拟器或者设备正在运行:

adb devices

如果没有正在运行的模拟器,你可以使用下列命令启动一个模拟器:

emulator.exe @avdname

@avdname参数是Android虚拟设备的名称。如果你记得已创建的虚拟设备的名称,可以通过一下命令列出所有虚拟设备的信息:

android   list    avd

这个命令可以列出所有可用的虚拟设备。如果你想通过Eclipse ADT开发或者运行Android应用程序,你需要配置一个最新版本的虚拟设备。以上命令将列出最新的虚拟设备。

这儿有一个使用列表命令的输出示例:

I:\android\tools>android list avd

Available Android Virtual Devices:

Name: avd

Path: I:\android\tools\..\avds\avd3

Target: Google APIs (Google Inc.)

Based on Android 1.5 (API level 3)

Skin: HVGA

Sdcard: 32M

---------

Name: titanium

Path: C:\Documents and Settings\Satya\.android\avd\titanium.avd

Target: Android 1.5 (API level 3)

Skin: HVGA

         需要指出的是AVD我们在第二章已经进行过详细的讲解。

         你还可以通过ADT启动模拟器。一旦模拟器启动处于运行状态,你可以再次通过一下命令列出所有正在运行的设备:

adb devices

         现在你将看到如下输出:

List of devices attached

emulator-5554 device

         你可以通过帮助命令查看更多的命令和选项:

adb help

         同样你可以访问一下网站查看关于adb运行时选项:

http://developer.android.com/guide/developing/tools/adb.html.

         可以使用adb在已连接的设备上打开shell

adb shell

         你可以输入以下命令查看可用的shell命令

         #ls /system/bin

         #号是shell的提示符。为了简短起见,在下面的示例中将忽略这个提示符。前面的代码输出的信息为:

dumpcrash                  

am

dumpstate

input

itr

monkey

pm

svc

ssltest

debuggerd

dhcpcd

hostapd_cli

fillup

linker

logwrapper

telnetd

iftop

mkdosfs

mount

mv

notify

netstat

printenv

reboot

ps

renice

rm

rmdir

rmmod

sendevent

schedtop

ping

 

sh

hciattach

sdptool

logcat

servicemanager

dbus-daemon

debug_tool

flash_image

installd

dvz

hostapd

htclogkernel

mountd

qemud

radiooptions

toolbox

hcid

route

setprop

sleep

setconsole

smd

stop

top

start

umount

vmstat

wipe

watchprops

sync

netcfg

chmod

date

dd

cmp

cat

dmesg

df

getevent

getprop

hd

id

ifconfig

insmod

ioctl

kill

ln

log

lsmod

ls

mkdir

dumpsys

service

playmp3

sdutil

rild

dalvikvm

dexopt

surfaceflinger

app_process

mediaserver

system_server

 

使用如下shell命令可以查看根目录下的文件夹和文件

ls      -l

 

你需要访问这个文件夹查看数据库列表

ls      /data/data

 

这个目录包含了在设备上安装程序的列表。让我们来查看com.android.providers.contacts包:

ls      /data/data/com.android.providers.contacts/databases

 

这个命令列出一个叫做contacts.db的SQLite数据库文件。(值得注意的是,在Android中数据库可能是在程序第一次运行的时候创建的,如果你没看到这个文件表明你从来没有运行过联系人程序)

 

如果包含一个查找命令,你就可以查看所有的*.db文件。但是除了ls以外没有更好的方法了。最简便的方法是:

ls  -R /data/data/*/databases

 

使用这个命令,你将看到Android列出所有的数据库(这里只列举一些常见得,你的设备上可能还有很多)

alarms.db

contacts.db

downloads.db

internal.db

settings.db

mmssms.db

telephony.db

 

你可以调用sqlite3去查看这些数据库

sqlite3    /data/data/com.android.providers.contacts/databases/contacts.db

 

退出则采用如下命令

sqlite>.exit

 

adb的提示符为#号而sqlite3的提示符为sqlite>。你可以再http://www.sqlite.org/sqlite.html这个网站上浏览关于sqlite3的各种命令。但是,我们会在这里列举出一些重要的命令,所以你不是必须去网上浏览。你可以键入以下命令查看数据库中所有的表:

sqlite>. tables

这个命令是以下SQL语句的缩写

select name from sqlite_master where type in(‘table’,’view’) and name not like ‘sqlite_%’ union all select name from sqlite_temp_master where type in (‘table’,’view’) order by 1


可以猜测书,sqlite_master表示一个管理表,管理了数据库中大量的表和视图。下列命令会打印出contacts.db数据库中people表的创建语句:

.schema people

这种方法可以在SQLite中查看表结构。在使用内容提供者的时候,你需要注意这些字段的类型,因为访问方法需要传入合适的参数。

但是,通过很长的创建语句去了解字段的名称以及他们的类型是非常让人厌烦的事情。所以,还有一种简便的方法:你可以导出contacts.db数据库文件到你的计算机上,然后使用图形化工具SQLite version 3去查看数据库信息。你可以使用一下命令导出contacts.db文件:

adb pull /data/data/com.android.providers.contacts/databases/contacts.db

c:/somelocaldir/contacts.db

还可以使用免费的Sqliteman(http://sqliteman.com),它是针对SQLite数据库的图形化工具。

 

Sqlite快速入门

下列的sql语句可以帮助你快速的操作SQLite数据库:

//设置列头可见

sqlite> .headers on

 

//查询表中所有的行

select * from table1;

 

//统计表中行的数量

select count(*) from table1;

 

//查询特定的列

select col1,col2 from table1;

 

//查询不重复的列数据

select distinct col1 from table1;

 

//查询不重复数据的数量

select count(col1) from (select distinct col1 from table1);

 

//分组查询

select count(*), col1 from table1 group by col1;

 

//内连接

select * from table1 t1,table2 t2 where t1.col1 = t2.col1;

 

//左外连接

Select * from table1 t1 left join table2 t2 on t1.col1 = t2.col1 where.. ..

 

内容提供者的设计思想

你现在已经知道如何使用Android和SQLite工具查看存在的内容提供者。接下来,我们将详细讲解内容提供者得构成元素以及如何使用内容提供者抽象的访问其它应用程序的数据。

 

总的说来,内容提供者与以下抽象思维比较接近:

网站

代表性的状态传输

Web Services

存储过程

设备上的每一个内容提供者注册它自己就像网站注册域名。从提供商可提供的大量的URI中选择一个唯一标识的字符串作为域名。这可不像一个有域名的网站提供大量的URL去展示它的文章以及其他内容。

 

这个凭证注册发证在AndroidManifest.xml文件中。这里有两个示例告诉你如何在AndroidManifest.xml中注册提供者:

<provider android:name="SomeProvider"

android:authorities="com.your-company.SomeProvider" />

<provider android:name="NotePadProvider"

android:authorities="com.google.provider.NotePad"/>

 

一个内容提供者得凭证就好比一个网站的域名。在给出的注册凭证之前你需要加上凭证前缀,例如:

content://com.your-company.SomeProvider/

content://co.google.provider.NotePad/

 

内容提供者也提供像REST(表述性状态转移)的URL来检索或者操作数据。对于前面的注册,在NotePad提供的数据库中通过如下URI来识别目录或者是notes数据的集合:

content://com.google.provider.NotePad/Notes

识别特殊的note的URI如下:

content://com.google.provider.NotePad/Notes/#

其中#号代表了note的id。这有一些访问内容提供者数据的URI的示例:

content://media/internal/images

content://media/external/images

content://contacts/people/

content://contacts/people/23

 

值得注意的是,这里的URI”media”(content://media)和”contact”(content://contacts)都不是一个完整的结构。这是因为他们不是第三方提供的,而是由Android控制的。

 

内容提供者也显示出了web services的特点。内容提供者通过URI,像service一样公开内部数据。然而,通过内容提供者的URI得到的不是键入的数据,是基于SOAP的web service的调用。这个数据更像通过JDBC语句获得的结果集数据。即使与JDBC的概念很相似,但是我们不希望让你觉得内容提供者与JDBC的结果集是一样的。

 

调用者需要预先了解返回的行与列的结构。你可以阅读“Android的MIME Type结构”一节,内容提供者有一个内建的结构允许你判断URI代表的数据的MIME  type。

 

除了与网站,REST,和web services相似以外,内容提供者得URI也与数据库中的存储过程名字类似。存储过程是基于服务的访问底层关系数据。URI与存储过程相似,因为URI调用内容提供者返回一个游标。然而,内容提供者与存储过程也有不同之处,存储过程需要传入输入参数,而调用内容提供者的输入参数包含在URI中。

 

以上提供了一些比较,希望能够帮助你更好的理解内容提供者。

 

Android URI的结构

我们之所以拿内容提供者与网站进行比较,是因为它对于传入的URI进行相应。所以,你需要调用URI才能检索内容提供者的数据。内容提供者中检索的数据是一个行和列的集合,在Android中叫做游标(cursor)对象。在这种情况下,想要检索数据,首先得研究URI的结构。

 

在Android中内容提供者得URI与HTTP的URI很相似,除了它是由content开头的:

content://*/*/*

或者

content://authority-name/path-segment1/path-segments/etc….

 

下面是一个能够从notes数据库中识别编号为23的数据

content://com.google.provider.NotePad/notes/23

 

在content://后面,URI包含了一个权限的唯一的标示,这个标识就是在应用程序中内容提供者得注册。在前面的示例中,com.google.provider.NotePad就是URI的权限部分。

 

/notes/23是特定于每个提供者的路径部分。notes和23部分被称为路径段。它的责任是提供文档,解释路径,以及URI的路径段部分。

 

内容提供者的开发人员通常在实现内容提供者的Java包中的类以及接口中定义常量来表示路径段。说的更详细点,路径的第一部分可能指向一个对象的集合。例如,/notes表明一个集合或者是notes的目录,而/23引用一个特殊的note项。

 

鉴于此URI,提供者被希望于通过特定的URI标识检索出数据行。提供者也被寄希望于可以通过在此URI上使用任何状态改变的方法:insert,update 获得delete来修改数据。

 

Android MIME Types结构

正如网站通过给定的URL返回一个MIME Type(这个类型可以让浏览器采用正确的程序呈现内容),内容提供者也添加了通过URI返回MIME type的能力。这样就可以灵活的查看数据。只要知道数据的种类,你可能有一个或者多个程序知道如何处理这个数据。例如,如果你的硬盘上有一个文本文件,有许多编辑器可以显示这个文本文件。操作系统甚至可能给你一个选择,使用哪个编辑器去打开这个文本文件。

在android中MIME type机制与在HTTP中是一样的。提供者支持给定MIME type的URI,并且提供者返回的一个MIME type 有两部分字符串标根据标准的网页MIME协议标识它饿的MIME类型,你可以在下面网址中查看到标准的MIME类型:

http://tools.ieft.org/html/rfc2046

 

根据MIME类型的描述,一个MIME类型包含两部分:类型和子类型。这是一些已知的MIME类型对:

text/html

text/css

text/xml

text/vnd.curl

application/pdf

application/rtf

application/vnd.ms-excel

 

你可以通过互联网数字分配机构(IANA)的网站上看到完整的已注册的类型和子类型的列表:

http://www.iana.org/assignments/media-types/

 

主要注册的内容类型有:

application

audio

example

image

message

model

multipart

text

video

 

这些主要的类型都有子类型。但是,如果供应商有专有的数据格式,那么子类型的名字会以vnd开头。例如,Microsoft Excel的子类型是vnd.ms-excel,而pdf没有供应商的标准的子类型,所以不会有vnd前缀。

 

一些子类型是以x-开头的,这些子类型没有注册,所以不是标准的子类型。它们被认为是合作代理双边定义的私有的值。下面是几个例子:

application/x-tar

audio/x-aiff

video/x-msvideo

 

Android遵循如下约定来定义MIME类型。vnd在Android的MIME中表明这些类型以及子类型不是标准的,供应商的具体形式。为了提供独特的MIME类型,Android进一步使用类似域名规范的多个部分标定类型和子类型。此外,Android MIME类型对每个内容类型有两种形式:一种为特定的记录,另一种为多个记录。

单条记录的MIME类型如下:

vnd.android.cursor.item/vnd.yourcompanyname.contenttype

 

多条记录的MIME类型如下:

vnd.android.cursor.dir/vnd.yourcompanyname.contenttype

 

这里有一些示例:

//单个note

vnd.android.cursor.item/vnd.google.note

 

//多个note

vnd.android.cursor.dir/vnd.google.note

 

MIME类型被广泛的用于Android,特别是intent,其中系统通过MIME type的数据指定调用哪个activity。MIME类型无不源于内容提供者提供的URI。你在使用MIME类型的时候需要抓住三个要点:

n  类型和子类型必须有唯一的指代。类型几乎由你自己决定。它主要是一种单个或多个的目录。在Android的环境中,这些可能没有你想想的那么开放。

 

n  类型和子类型如果他们不是标准的,他们需要在之前加上vnd。

 

n  他们是根据你的特殊需要有特殊的命名空间

 

需要重申一下,通过Android游标返回的集合数据的主要MIME类型总是vnd.android.cursor.dir,通过Android游标返回的单个数据的主要MIME类型总是vnd.android.vursor.item。当涉及到子类型的时候,你会有更多的回旋余地。如vnd.google.note,在vnd.后面的部分,你可以自由的选择子类型。

 

使用URI读取数据

现在你知道需要通过内容提供者提供的URI才能检索数据。因为这些URI定义都是唯一的,所以重要的是我们需要在调用之前查阅相关文档。Android中使用如下URI字符串提供了一些内容提供者的。

 

这些URI被定义在Android SDK的帮助类中:

MediaStore.Images.Media.INTERNAL_CONTENT_URI

MediaStore.Images.Media.EXTERNAL_CONTENT_URI

Contacts.People.CONTENT_URI

 

实际对应的值如下:

content://media/internal/images

content://media/external/images

content://contacts/people/

 

MediaStore提供者定义了两个URI,Contacts提供者定义了一个URI。如果你细心的话,你会发现这些常量被定义在一个等级模式下。例如:联系人的URI是Contacts.People.CONTENT_URI。是因为联系人的数据库中可能包含许多表来描述联系人的信息。People是其中一个表。数据库中每一个重要的实体都可能有自己的内容URI,但是,所有的URI都是已权限名为基础的。

 

通过给定的URI,使用内容提供者检索单个数据的代码如下:

Uri peopleBaseUri = Contacts.People.CONTENT_URI;

Uri myPersonUri = Uri.withAppendedPath(Contacts.People.CONTENT_URI, "23");

//查询这条记录

//managedQuery是activity的一个方法

Cursor cur = managedQuery(myPersonUri, null, null, null);

 

请注意Contacts.People.CONTENT_URI是预定在People类中的一个常量。在这个例子中,代码使用根URI,增加了一个具体的person的ID给它,然后调用managedQuery方法。

 

作为使用URI检索数据的一部分,数据的排序,选择要查询的列,以及查询条件。在上例中都设置为null;

 

上述代码获得了一个Cursor对象,一下代码给出了如果从Cursor对象中获得数据。

// 定义对应的列名

string[] projection = new string[] {

People._ID,

People.NAME,

People.NUMBER,

};

// 获得联系人Perple的URI

// content://contacts/people/

Uri mContactsUri = Contacts.People.CONTENT_URI;

// 使用最好的方式检索数据

Cursor managedCursor = managedQuery( mContactsUri,

projection, //需要检索出那些列

null, // 检索条件

Contacts.People.NAME + " ASC"); //排序

 

请注意,projection是一个代表了列名的字符串数组。所以,除非你知道这些列名,否则你会发现很难创建projection。你可以再提供URI的同一个类中查看这些列名,由此,查看People类,你可以看到这些列名的定义:

DISPLAY_NAME

LAST_TIME_CONTACTED

NAME

NOTES

PHOTO_VERSION

SEND_TO_VOICE_MAIL

STARRED

TIMES_CONTACTED

 

你可以在SDK文档的android.provider.Contacts.PerpleColumns类中查看到更多的列。你可以使用一下URL:

http://developer.android.com/reference/android/provider/Contacts.PeopleColumns.html

 

正如前面所说的,联系人的数据库包含了许多张表,每个表都有一个类或者借口来描述它的列和类型。让我们根据一下URL参阅android.providers.Contacts文档:

http://developer.android.com/reference/android/provider/Contacts.html

 

你可以看到这个包下包含了这些类和接口

ContactMethods

Extensions

Groups

Organizations

People

Phones

Photos

Settings

 

每个类都代表了contacts.db数据库中的一个表,并且每个表都有代表自己的URI。另外,每个类中都定义了标识列名的列接口,例如:PeopleColumns。

 

让我们重新回到游标,它包含了零条或者多条记录。列顺序和类型特定于内容提供者。然而,每一行都有一个默认的列_id为每一个的唯一标识。

 

使用游标

这里有关于游标的一些特点:

游标是行数据的集合

在使用游标读取数据之前,你需要使用moveToFirst()方法。因为游标的起始位置在第一行数据之前。

需要知道列名

需要知道列的类型

所有的操作都居于列号,所以你必须把列名转换成列号

游标是任意游标(你可以向前,向后读取,也可以越过几行进行读取)

因为游标是任意游标,所以你必须通过行数量来访问。

 

Android提供了一组方法用于操作游标。下面的代码用于判断游标是否是一个空游标,如果不为空,则逐行读取数据。

 

if (cur.moveToFirst() == false)

{

//为空游标

return;

}

//游标已经指向了第一行数据

int nameColumnIndex = cur.getColumnIndex(People.NAME);

String name = cur.getString(nameColumnIndex);

//循环游标

while(cur.moveToNext())

{

//访问字段数据

}

 

以上代码中,游标指向第一行数据之前,为了能够让游标指向第一行数据,需要调用moveToFirst()方法。如果游标为空,则该方法返回false。如果返回true就代表成功指向第一行数据,接下来就可以使用moveToNext()方法进行逐行读取。

 

为了让你获得游标当前的状态,Android提供了如下方法:

isBeforeFirst()

isAfterLast()

isClosed()

 

使用这些方法,你可以写出for循环来代替while循环:

//获得列号

int nameColumn = cur.getColumnIndex(People.NAME);

int phoneColumn = cur.getColumnIndex(People.NUMBER);

//逐行读取游标数据

for(cur.moveToFirst();!cur.isAfterLast();cur.moveToNext())

{

String name = cur.getString(nameColumn);

String phoneNumber = cur.getString(phoneColumn);

}

 

直接使用列索引似乎有点武断。因此,我们建议您首先从游标中获得列号,避免意外发生。要获得游标中数据行的数量,Android提供了一个叫做getCount()的方法,该方法返回游标中数据行的总数。

 

使用查询条件

内容提供者提供了两种方式传递查询条件:

n  通过URI传递查询条件

n  通过条件语句设置条件,并且由一个字符串数据给条件设置值

我们将会通过同样的代码功能对比讲解这两种方式实现条件查询。

 

通过URI传递查询调教

想象一下,你想要从google notes的数据库中检索出编号(id)为23的note信息。你可以使用下列代码获得一个包含改行数据的游标:

Activity someActivity;

//..initialize someActivity

String noteUri = "content://com.google.provider.NotePad/notes/23";

Cursor managedCursor = someActivity.managedQuery( noteUri,

projection, //Which columns to return.

null, // WHERE clause

null); // Order-by clause.

 

在这个示例中,managedQuery方法代表查询条件的参数传入的值为null是因为URI已经指明了我们需要查找数据的编号。这个编号(id)包含在URI中。当我们使用这个URI的时候已经传递了查询条件了。如果你注意到notes的内容提供者是如何实现相应的查询方法的,就能很清楚的了解为什么能这样做了。下面是查询方法的代码片段:

//从与下列类似的URI中取得note编号

//content://.../notes/23

int noteId = uri.getPathSegments().get(1);

//请求查询构造器构造查询语句

//指定表明

queryBuilder.setTables(NOTES_TABLE_NAME);

//使用noteId作为查询条件

queryBuilder.appendWhere(Notes._ID + "=" + noteId);

 

请注意note的编号(id)是如何从URI中提取的。在代表传入的URI的Uri类中有一个提取URI根(content://com.google.provider.NotePad)后面部分的方法。这些部分被称为路径段。他们之间用/分隔,例如:/seg1/seg3/seg4。他们根据他们所在的位置按索引顺序排列。在上述URI中,索引为1的路径段的值为23。然后使用QueryBuilder类指定ID等于23为条件,最后执行的查询语句入下:

select * from notes where _id = 23

 

指定查询条件

你已经知道如何使用URI传递查询条件了,Android支持一些其它的方式来传递特定列作为查询条件。为了更好的理解,先来了解一下Activity的managedQuery方法:

public final Cursor managedQuery(Uri uri,

String[] projection,

String selection,

String[] selectionArgs,

String sortOrder)

 

请注意名为selection的字符串类型的参数。这个字符串参数需要传入与SQL的查询条件类似的过滤条件。传入null值将不带任何条件查询出所有数据。在这个参数中可以使用?号作为条件值的占位符。这些占位符的值将与selectionArgs这个字符串数组绑定。

 

因为存在两种指定查询条件的方式,你可能会很难确定提供者是如何使用这些查询条件的,以及如果同时使用两种方式传入查询条件,那中会有更高的优先级别。

例如,你可以使用两种方法查询出编号(ID)为23的note:

//使用URI的方式

managedQuery("content://com.google.provider.NotePad/notes/23"

,null

,null

,null

,null);

或者

//指定查询条件的方式

managedQuery("content://com.google.provider.NotePad/notes"

,null

,"_id=?"

,new String[] {23}

,null);

一般来说,通常情况下建议使用URI的方式,如果有特殊情况则采用指定查询条件的方式。

 

插入数据

到目前为之,我么已经谈论了如何使用URI来检索内容提供者的数据。现在让我们把注意力集中到增加,修改,以及删除操作上。

 

Android使用android.content.ContentValues类代表一行要被插入的数据。ContentValues是一个键/值对的字典,键对应列名,值对应列的值。你在插入数据之前你需要先将记录保存到ContentValues对象中,然后调用android.content.ContentResolver类对象使用URI执行插入操作。

 

以下代码使用ContentValues向notes表中插入一行记录:

ContentValues values = new ContentValues();

values.put("title", "New note");

values.put("note","This is a new note");

//值对象已经插入到ContentValues中

 

可以使用Activity类获得一个ContentResolver的引用:

ContentResolver contentResolver = activity.getContentResolver();

 

到此,你只需要使用URI告诉ContentResolver进行插入操作。这些URI定义在于Notes表对应的类中。在NotePad的例子中,URI是:NotePad.Notes.CONTENT_URI

 

现在就可以使用这个URI结合ContentValues来完插入操作:

Uri uri = contentResolver.insert(Notepad.Notes.CONTENT_URI, values);

 

Insert方法返回一个Uri对象,这个Uri对象代表了新才插入的数据行。这个URI的结构如下:

Notepad.Notes.CONTENT_URI/new_id

 

在内容提供者中添加一个文件

有些时候,你可能需要在数据库中存储一个文件。通常的做法是把这个文件保存到磁盘上,然后在数据库中更新记录来对应文件的名称,让数据库中的记录与该文件对应起来。

 

Android也是这样做的,并且它通过定义一个特定的存储过程自动的保存和检索这些文件。

在Android中,列名为_data的数据都是代表的是引用文件的名称。

 

当我们在表中插入一条数据,Android会返回一个URI给调用者。只要你使用这样的结构保存一条记录,你也需要在之后在当前的环境中保存文件。为了实现这样的操作,Android允许ContentResolver使用数据库记录的Uri并且返回一个可写的输出流。此时,系统会分配一个内部文件并且将文件名字的引用存储到_data字段。

 

假如你扩展Notepad的示例,需要为给定的note存储一个图片,你需要添加一个名为_data的列,并且首先执行insert操作后获得返回的URI。下列代表做出了示范:

ContentValues values = new ContentValues();

values.put("title", "New note");

values.put("note","This is a new note");

//使用一个内容提供者去插入记录

ContentResolver contentResolver = activity.getContentResolver();

Uri newUri = contentResolver.insert(Notepad.Notes.CONTENT_URI, values);

 

当你获得已插入记录的URI,下列代码调用ContentResolver去获得一个文件输出流的引用:

….

//使用ContentResolver直接获得一个输出流

//ContentResolver隐性的访问了_data列,在这个列中保存了真正的文件引用

OutputStream outStream = activity.getContentResolver().openOutputStream(newUri);

someSourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);

outStream.close();

 

接下来就可以使用输出流进行写操作了。

 

更新和删除

到目前为止,我们已经讨论过查询和添加操作。更新和删除都是非常明确的。执行一个更新操作与添加操作类似,使用ContentValues对象改变列的值。下列是使用ContentResolver对象更新的示例:

int numberOfRowsUpdated =

activity.getContentResolver().update(

Uri uri,

ContentValues values,

String whereClause,

String[] selectionArgs )

 

参数whereClause应该包含需要更新的行。删除操作也是如此:

int numberOfRowsDeleted =

activity.getContentResolver().delete(

Uri uri,

String whereClause,

String[] selectionArgs )

 

可以很清楚的看到,删除方法不需要ContentValues参数,因为在执行删除操作的时候不需要指定特定的列。

 

几乎managedQuery和ContentResolver的调用最终都直接调用内容提供者类。了解内容提供者是如何实现这些方法的,能帮助我们更好的去理解。在下一节,我们将涵盖一个叫做BookProvide的内容提供者是如何实现的示例。

 

实现内容提供者

我们已经讨论了如何操作内容提供者的数据,但是还没有讨论如何定义一个内容提供者。要定义一个内容提供者,你需要继承android.content.ContentProvider以及实现以下方法:

query

insert

update

getType

 

在实现这些方法之前,你需要学习一些知识。我们将以示例详细的说明实现一个内容提供者的步骤:

1.       准备数据库,URI,列名等等,然后创建一个定义类,定义所需要的元数据。

2.       继承抽象类ContentProvider

3.       实现query,insert,update,delete,和getType方法

4.       在manifest文件中注册内容提供者

 

准备数据库

首先,我们需要创建一个包含书籍信息的数据库。这个数据库(book)只包含一个叫做books的表,表中的列有:name,isbn和author。这些列名被定义在了metadata类中。在这个示例中定义了一个名为BookProviderMetaData的类用来定义常量信息,该类得代码如下:

public class BookProviderMetaData

{

public static final String AUTHORITY = "com.androidbook.provider.BookProvider";

public static final String DATABASE_NAME = "book.db";

public static final int DATABASE_VERSION = 1;

public static final String BOOKS_TABLE_NAME = "books";

private BookProviderMetaData() {}

//inner class describing BookTable

public static final class BookTableMetaData implements BaseColumns

{

private BookTableMetaData() {}

public static final String TABLE_NAME = "books";

//uri and MIME type definitions

public static final Uri CONTENT_URI =

Uri.parse("content://" + AUTHORITY + "/books");

public static final String CONTENT_TYPE =

"vnd.android.cursor.dir/vnd.androidbook.book";

public static final String CONTENT_ITEM_TYPE =

"vnd.android.cursor.item/vnd.androidbook.book";

public static final String DEFAULT_SORT_ORDER = "modified DESC";

//Additional Columns start here.

//string type

public static final String BOOK_NAME = "name";

//string type

public static final String BOOK_ISBN = "isbn";

//string type

public static final String BOOK_AUTHOR = "author";

//Integer from System.currentTimeMillis()

public static final String CREATED_DATE = "created";

//Integer from System.currentTimeMillis()

public static final String MODIFIED_DATE = "modified";

}

}

 

在BookProviderMetaData类中定义了权限名为:com.androidbook.provider.BookProvider。这个将作为在Android manifest文件中的注册字符串。这个字符串构成了URI的前一部分。

 

在这个类中还包含了一个名为BookTableMetaData的内部类。在这个内部类中定义了一个标识books所有数据的URI。这个URI如下所示:

content://com.androidbook.provider.BookProvider/books

 

这个URI值保存在常量BookProviderMetaData.BookTableMetaData.CONTENT_URI中。

 

在类BookTableMetaData中还定义了books表的所有数据以及单行数据的MIME类型。实现的内容提供者将会根据传入的URI返回这些常量。

 

然后BookTableMetaData中定义了一组列名:name,isbn,author ,createde(创建时间)和modified(最后一次更新时间),值得注意的是,在定义这些元数据的时候,类型要与数据库字段的类型保持一致。

 

BookTableMetaData类继承自BaseColumns类,在BaseColumns类中提供了标准的_id字段,这个字段代表了每行数据的ID。有了这些元数据的定义,我们已经实现了内容提供者。

 

扩展ContentProvider

为了实现BookProvider内容提供者得示例还需要继承ContentProvider类并且重写onCreate方法来创建数据库,然后实现query,insert,update,delete和getType方法。本节涵盖数据库建立的步骤,而下面的部分将会对于query,insert,update,delete和getType方法进行讲解。这些代码如下:

 

public class BookProvider extends ContentProvider

{

//Logging helper tag. No significance to providers.

private static final String TAG = "BookProvider";

 

//Setup projection Map

//Projection maps are similar to "as" (column alias) construct

//in an sql statement where by you can rename the

//columns.

private static HashMap<String, String> sBooksProjectionMap;

static

{

sBooksProjectionMap = new HashMap<String, String>();

sBooksProjectionMap.put(BookTableMetaData._ID,

BookTableMetaData._ID);

//name, isbn, author

sBooksProjectionMap.put(BookTableMetaData.BOOK_NAME,

BookTableMetaData.BOOK_NAME);

sBooksProjectionMap.put(BookTableMetaData.BOOK_ISBN,

BookTableMetaData.BOOK_ISBN);

sBooksProjectionMap.put(BookTableMetaData.BOOK_AUTHOR,

BookTableMetaData.BOOK_AUTHOR);

//created date, modified date

sBooksProjectionMap.put(BookTableMetaData.CREATED_DATE,

BookTableMetaData.CREATED_DATE);

sBooksProjectionMap.put(BookTableMetaData.MODIFIED_DATE,

BookTableMetaData.MODIFIED_DATE);

}

 

//Setup URIs

//Provide a mechanism to identify

//all the incoming uri patterns.

private static final UriMatcher sUriMatcher;

private static final int INCOMING_BOOK_COLLECTION_URI_INDICATOR = 1;

private static final int INCOMING_SINGLE_BOOK_URI_INDICATOR = 2;

static {

sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY, "books",

INCOMING_BOOK_COLLECTION_URI_INDICATOR);

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY, "books/#",

INCOMING_SINGLE_BOOK_URI_INDICATOR);

}

/**

* Setup/Create Database

* This class helps open, create, and upgrade the database file.

*/

private static class DatabaseHelper extends SQLiteOpenHelper {

DatabaseHelper(Context context) {

super(context,

BookProviderMetaData.DATABASE_NAME,

null,

BookProviderMetaData.DATABASE_VERSION);

}

@Override

public void onCreate(SQLiteDatabase db)

{

Log.d(TAG,"inner oncreate called");

db.execSQL("CREATE TABLE " + BookTableMetaData.TABLE_NAME + " ("

+ BookTableMetaData._ID + " INTEGER PRIMARY KEY,"

+ BookTableMetaData.BOOK_NAME + " TEXT,"

+ BookTableMetaData.BOOK_ISBN + " TEXT,"

+ BookTableMetaData.BOOK_AUTHOR + " TEXT,"

+ BookTableMetaData.CREATED_DATE + " INTEGER,"

+ BookTableMetaData.MODIFIED_DATE + " INTEGER"

+ ");");

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)

{

Log.d(TAG,"inner onupgrade called");

Log.w(TAG, "Upgrading database from version "

+ oldVersion + " to "

+ newVersion + ", which will destroy all old data");

db.execSQL("DROP TABLE IF EXISTS " +

BookTableMetaData.TABLE_NAME);

onCreate(db);

}

}

private DatabaseHelper mOpenHelper;

//Component creation callback

 

@Override

public boolean onCreate()

{

Log.d(TAG,"main onCreate called");

mOpenHelper = new DatabaseHelper(getContext());

return true;

}

@Override

public Cursor query(Uri uri, String[] projection, String selection,

String[] selectionArgs, String sortOrder)

{

SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

switch (sUriMatcher.match(uri)) {

case INCOMING_BOOK_COLLECTION_URI_INDICATOR:

qb.setTables(BookTableMetaData.TABLE_NAME);

qb.setProjectionMap(sBooksProjectionMap);

break;

case INCOMING_SINGLE_BOOK_URI_INDICATOR:

qb.setTables(BookTableMetaData.TABLE_NAME);

qb.setProjectionMap(sBooksProjectionMap);

qb.appendWhere(BookTableMetaData._ID + "="

+ uri.getPathSegments().get(1));

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

// If no sort order is specified use the default

String orderBy;

if (TextUtils.isEmpty(sortOrder)) {

orderBy = BookTableMetaData.DEFAULT_SORT_ORDER;

} else {

orderBy = sortOrder;

}

// Get the database and run the query

SQLiteDatabase db = mOpenHelper.getReadableDatabase();

Cursor c = qb.query(db, projection, selection,

selectionArgs, null, null, orderBy);

//example of getting a count

int i = c.getCount();

// Tell the cursor what uri to watch,

// so it knows when its source data changes

c.setNotificationUri(getContext().getContentResolver(), uri);

return c;

}

@Override

public String getType(Uri uri)

{

switch (sUriMatcher.match(uri)) {

case INCOMING_BOOK_COLLECTION_URI_INDICATOR:

return BookTableMetaData.CONTENT_TYPE;

case INCOMING_SINGLE_BOOK_URI_INDICATOR:

return BookTableMetaData.CONTENT_ITEM_TYPE;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

}

@Override

public Uri insert(Uri uri, ContentValues initialValues)

{

// Validate the requested uri

if (sUriMatcher.match(uri)!= INCOMING_BOOK_COLLECTION_URI_INDICATOR)

{

throw new IllegalArgumentException("Unknown URI " + uri);

}

ContentValues values;

if (initialValues != null) {

values = new ContentValues(initialValues);

} else {

values = new ContentValues();

}

Long now = Long.valueOf(System.currentTimeMillis());

// Make sure that the fields are all set

if (values.containsKey(BookTableMetaData.CREATED_DATE) == false)

{

values.put(BookTableMetaData.CREATED_DATE, now);

}

if (values.containsKey(BookTableMetaData.MODIFIED_DATE) == false)

{

values.put(BookTableMetaData.MODIFIED_DATE, now);

}

if (values.containsKey(BookTableMetaData.BOOK_NAME) == false)

{

throw new SQLException(

"Failed to insert row because Book Name is needed " + uri);

}

if (values.containsKey(BookTableMetaData.BOOK_ISBN) == false) {

values.put(BookTableMetaData.BOOK_ISBN, "Unknown ISBN");

}

if (values.containsKey(BookTableMetaData.BOOK_AUTHOR) == false) {

values.put(BookTableMetaData.BOOK_ISBN, "Unknown Author");

}

SQLiteDatabase db = mOpenHelper.getWritableDatabase();

long rowId = db.insert(BookTableMetaData.TABLE_NAME,

BookTableMetaData.BOOK_NAME, values);

if (rowId > 0) {

Uri insertedBookUri =

ContentUris.withAppendedId(

BookTableMetaData.CONTENT_URI, rowId);

getContext()

.getContentResolver()

.notifyChange(insertedBookUri, null);

return insertedBookUri;

}

throw new SQLException("Failed to insert row into " + uri);

}

@Override

public int delete(Uri uri, String where, String[] whereArgs)

{

SQLiteDatabase db = mOpenHelper.getWritableDatabase();

int count;

switch (sUriMatcher.match(uri)) {

case INCOMING_BOOK_COLLECTION_URI_INDICATOR:

count = db.delete(BookTableMetaData.TABLE_NAME,

where, whereArgs);

break;

case INCOMING_SINGLE_BOOK_URI_INDICATOR:

String rowId = uri.getPathSegments().get(1);

count = db.delete(BookTableMetaData.TABLE_NAME,

BookTableMetaData._ID + "=" + rowId

+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),

whereArgs);

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

getContext().getContentResolver().notifyChange(uri, null);

return count;

}

@Override

public int update(Uri uri, ContentValues values,

String where, String[] whereArgs)

{

SQLiteDatabase db = mOpenHelper.getWritableDatabase();

int count;

switch (sUriMatcher.match(uri)) {

case INCOMING_BOOK_COLLECTION_URI_INDICATOR:

count = db.update(BookTableMetaData.TABLE_NAME,

values, where, whereArgs);

break;

case INCOMING_SINGLE_BOOK_URI_INDICATOR:

String rowId = uri.getPathSegments().get(1);

count = db.update(BookTableMetaData.TABLE_NAME,

values, BookTableMetaData._ID + "=" + rowId

+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),

whereArgs);

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

getContext().getContentResolver().notifyChange(uri, null);

return count;

}

}

 

查询方法需要需要查询的列名。这和查询语句需要列名是一样的。Android使用一个叫做projection的map对象代表这些列或者这些列的别名。我们在使用实现的查询方法之前需要构建一个这样的map对象。代码入上所示

 

大多数方法的实现都需要传入URI作为参数。尽管这个内容提供者的所有URI都是有相同的开头部分,但是尾部却是不同的,这就跟网址一样。每个URI,虽然它们开头部分相同,但是一定有不同地方来标识不同的数据或者文档。举例说明:

Uri1: content://com.androidbook.provider.BookProvider/books

Uri2: content://com.androidbook.provider.BookProvider/books/12

 

可以看到BookProvider需要区分每个URI。这是一个简单的例子。如果BookProvider不仅只包含books对象,那么将会有更多的URI去标识不同的对象。

 

内容提供者的实现需要一个区分不同URI的机制。Android使用一个叫做UriMatcher的类来处理。所以我们需要使用所有包含的URI来构建此类对象。你可以在BookProvider类中创建projection map之后查看到代码示例。接下来我们将在“使用UriMatcher识别URI”节中对UriMatcher类进行讲解。

 

 

实现MIME类型

BookProvider内容提供者必须实现getType()方法以返回给定URI的MIME类型。这个方法与一些其它内容提供者的方法一样——需要传入URI的重载方法。这个方法的首要任务是区分URI的类型。它是一个集合还是单行数据。

 

根据前面的内容,我们将使用UriMatcher去区分URI的类型。在BookTableMetaData类中已经定义了每类URI所需要返回的MIME类型。你可以查看在BookProvider类中查看这个方法的代码。

 

实现查询方法

内容提供者的query方法根据URI和查询条件返回查询到得结果行的集合。

 

和其它方法一样,query方法使用UriMatcher去辨别URI类型。如果URI类型是单一项类型的(single-item type),这个方法通过如下方式获得book ID。

1.       使用getPathSegments()方法提取路径段

2.       第一个索引段即为book ID

query方法根据projections参数返回对应的列的数据。最后,query方法返回给调用者一个cursor对象。在这个过程中,query方法使用SQLiteQueryBuilder对象表示和执行查询。

 

实现Insert方法

insert方法在内容提供者中负责将记录保存到底层数据库然后返回表示该条记录的URI。

与其它方法一样,insert方法使用UriMatcher识别URI类型。代码首先检查URI是否是一个表示集合类型的URI。如果不是,将抛出一个异常。

接下来代码将对各个列的参数值进行验证,如果某些列缺少值,代码可以为它赋一个合适的默认值。

接下来,代码使用SQLiteDataBase对象插入一行新记录并且返回新纪录的ID。最后代码通过返回的新ID构建一个新的URI。

 

实现更新方法

在内容提供者中update方法负责根据传入的列值以及更新条件更新对应列的数据。然后该方法返回已更新的记录行数。

和其它方法一样,update使用UriMatcher表示URI类型。如果URI类型是一个集合,那么根据传入的更新条件来更新多行数据。如果URI是单行记录类型,book ID在URI中指定以及指定更新条件。最后,该方法返回更新的行数。在第二十一章,将充分的说明notifyChange方法的含义。还要注意的是,notifyChange方法是如何宣布改URI对应的数据已经改变了的。也就是说,你可以再插入数据的方法中也调用notifyChange方法,表示在增加记录以后

…/books URI所代表的数据已经改变了。

 

实现删除方法

delete方法在内容提供者中负责根据传入的删除条件删除记录。该方法返回删除记录的条数。

与其它方法一样,delete方法使用UriMatcher标识URI类型。如果URI类型是一个集合,则该通过删除条件删除数据。如果删除条件为null,所有的记录都将被删除。如果Uri类型是单行记录类型,通过URI提取book ID并且指定删除条件。最后,该方法返回删除了的记录的数量。

 

使用UriMatcher识别URI

到目前为止,我们已经多次提到UriMatcher类。让我们深入的研究一下该类。几乎内容提供者中所有的重载方法都需要一个URI类型的参数。例如,调用query方法获得多行还是者单行记录是有传入的URI的类型决定的。在Android中的UriMatcher类能够帮助你识别URI类型。

 

该类是这样工作的:首先,你得构建一个UriMatcher实例,使用该实例注册需要的URI类型的样式。其次,为每个样式分配一个唯一的整数值。一旦这些样式被成功设置到该类实例了,当一个匹配样式的URI传入的时候,你就可以使用UriMatcher进行区分。

 

我们已经讨论过,BookProvier内容提供者有两种URI样式:一种针对集合,一种针对单行数据。在下面的代码中使用UriMatcher实例注册这两种样式。并且为集合样式分配整数值1,为单行数据分配整数值2。

private static final UriMatcher sUriMatcher;

//define ids for each uri type

private static final int INCOMING_BOOK_COLLECTION_URI_INDICATOR = 1;

private static final int INCOMING_SINGLE_BOOK_URI_INDICATOR = 2;

static {

sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

//Register pattern for the books

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY

, "books"

, INCOMING_BOOK_COLLECTION_URI_INDICATOR);

//Register pattern for a single book

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY

, "books/#",

INCOMING_SINGLE_BOOK_URI_INDICATOR);

}

 

之后,你可以使用如下代码在实现内容提供者的重载方法中对于URI参数的类型样式进行判断:

switch (sUriMatcher.match(uri)) {

case INCOMING_BOOK_COLLECTION_URI_INDICATOR:

……

case INCOMING_SINGLE_BOOK_URI_INDICATOR:

……

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

请注意,UriMatcher类得match方法返回之前为其分配的对应的整数值。UriMatcher的构造方法使用一个整型数据来创建URI树的根节点。如果提供的URI既没有权限部分,也没有路径段,UriMatcher就返回改整数值。当传入的URI没有匹配项的时候也返回NO_MATCH。如果使用空参的构造函数构造一个UriMatcher对象,默认使用NO_MATCH常量初始化该对象。所以你可以使用如下代码代替上面的示例:

static {

sUriMatcher = new UriMatcher();

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY

, "books"

, INCOMING_BOOK_COLLECTION_URI_INDICATOR);

sUriMatcher.addURI(BookProviderMetaData.AUTHORITY

, "books/#",

INCOMING_SINGLE_BOOK_URI_INDICATOR);

}

 

使用Projection Maps

内容提供者扮演着抽象的列与真实数据库中列的桥梁的角色,当然,这些列可能不太一样。当构建查询的操作的时候,你必须将制定查询的列与数据库的列对应起来。构建projection map对使用SQLiteQueryBuilder类型是非常有帮助的。

 

在Android SDK文档中对于QueryBuilder类中的public void setProjectionMap(Map columnMap)方法进行了如下描述:

 

为查询设置一个特定的映射。改映射是调用者传入的列名与数据库列名之间的对应关系。这非常有助于在连接查询时重命名列名。例如你可以使用“name”对应查询语句中的“people.name”。如果使用这个映射,即使键和值是一样的,也要在这个映射中包括你想要检索的所有列名。

 

下面是在BookProvider内容提供者中构建的projection map:

sBooksProjectionMap = new HashMap<String, String>();

sBooksProjectionMap.put(BookTableMetaData._ID, BookTableMetaData._ID);

//name, isbn, author

sBooksProjectionMap.put(BookTableMetaData.BOOK_NAME

, BookTableMetaData.BOOK_NAME);

sBooksProjectionMap.put(BookTableMetaData.BOOK_ISBN

, BookTableMetaData.BOOK_ISBN);

sBooksProjectionMap.put(BookTableMetaData.BOOK_AUTHOR

, BookTableMetaData.BOOK_AUTHOR);

//created date, modified date

sBooksProjectionMap.put(BookTableMetaData.CREATED_DATE

, BookTableMetaData.CREATED_DATE);

sBooksProjectionMap.put(BookTableMetaData.MODIFIED_DATE

, BookTableMetaData.MODIFIED_DATE);

And then the query builder uses the variable sBooksProjectionMap like this:

queryBuilder.setTables(BookTableMetaData.TABLE_NAME);

queryBuilder.setProjectionMap(sBooksProjectionMap);

 

注册内容提供者

最后,你必须在Android.Manifest.xml文件中使用一下标签结构注册内容提供者:

<provider android:name=".BookProvider"

android:authorities="com.androidbook.provider.BookProvider"/>

 

使用Book Provider

完成以上步骤就已经创建了一个内容提供者,我们将给出执行这个内容提供者的示例。在这个示例中包括了添加书籍,删除书籍,获得书籍数量,以及显示所有书籍。

需要注意的是这个示例代码只是各个操作的核心代码,如果需要编译运行,请构建完整工程。

在本章的最后,包含了这个示例代码工程的代码下载连接。你可以在下载此工程后使用eclipse编译运行该工程。

 

添加书籍

这段代码往book数据库中插入一条新记录。

public void addBook(Context context)

{

String tag = "Exercise BookProvider";

Log.d(tag,"Adding a book");

ContentValues cv = new ContentValues();

cv.put(BookProviderMetaData.BookTableMetaData.BOOK_NAME, "book1");

cv.put(BookProviderMetaData.BookTableMetaData.BOOK_ISBN, "isbn-1");

cv.put(BookProviderMetaData.BookTableMetaData.BOOK_AUTHOR, "author-1");

ContentResolver cr = context.getContentResolver();

Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;

Log.d(tag,"book insert uri:" + uri);

Uri insertedUri = cr.insert(uri, cv);

Log.d(tag,"inserted uri:" + insertedUri);

}

 

删除书籍

一下代码从book数据库中三处最后一条书籍记录。在这段代码中getCount()方法在“获得书籍数量”中给出。

public void removeBook(Context context)

{

String tag = "Exercise BookProvider";

int i = getCount(context); //在下个示例中给出该方法代码

ContentResolver cr = context.getContentResolver();

Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;

Uri delUri = Uri.withAppendedPath(uri, Integer.toString(i));

Log.d(tag, "Del Uri:" + delUri);

cr.delete(delUri, null, null);

Log.d(tag, "New count:" + getCount(context));

}

 

请注意,这只是一个使用URI删除的简单示例。在该示例中获得最后一条记录URI的方法并不适用于所有情况下。尽管这样,当你已经添加5条记录它可以从最后一条开始进行逐条删除。在真实的案例中,你可能希望使用列表显示所有的记录,然后让用户选择删除哪条记录。根据用户的选择,你将获得需要删除数据的URI。

 

获得书籍数量

这段代码获得一个游标然后通过游标获得记录的数量。

private int getCount(Context context)

{

Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;

Activity a = (Activity)context;

Cursor c = a.managedQuery(uri,

null, //projection

null, //selection string

null, //selection args array of strings

null); //sort order

int numberOfRecords = c.getCount();

c.close();

return numberOfRecords;

}

 

显示书籍列表

下列代码检索book数据库中的所有数据。

public void showBooks(Context context)

{

String tag = "Exercise BookProvider";

Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;

Activity a = (Activity)context;

Cursor c = a.managedQuery(uri,

null, //projection

null, //selection string

null, //selection args array of strings

null); //sort order

int iname = c.getColumnIndex(

BookProviderMetaData.BookTableMetaData.BOOK_NAME);

int iisbn = c.getColumnIndex(

BookProviderMetaData.BookTableMetaData.BOOK_ISBN);

int iauthor = c.getColumnIndex(

BookProviderMetaData.BookTableMetaData.BOOK_AUTHOR);

//Report your indexes

Log.d(tag,"name,isbn,author:" + iname + iisbn + iauthor);

//walk through the rows based on indexes

for(c.moveToFirst();!c.isAfterLast();c.moveToNext())

{

//Gather values

String id = c.getString(1);

String name = c.getString(iname);

String isbn = c.getString(iisbn);

String author = c.getString(iauthor);

//Report or log the row

StringBuffer cbuf = new StringBuffer(id);

cbuf.append(",").append(name);

cbuf.append(",").append(isbn);

cbuf.append(",").append(author);

Log.d(tag, cbuf.toString());

}

//Report how many rows have been read

int numberOfRecords = c.getCount(context);

Log.d(tag,"Num of Records:" + numberOfRecords);

//Close the cursor

//ideally this should be done in

//a finally block.

c.close();

}

 

 

资源

这里提供一些附加Android资源,希望能帮助你更好的理解本章内容:

http://developer.android.com/guide/topics/providers/contentproviders.html

你可以通过此链接阅读内容提供者部分的Android文档。

http://developer.android.com/reference/android/content/ContentProvider.html

这是ContentProvider的API文档。

http://developer.android.com/reference/android/content/UriMatcher.html

改链接帮助您更好的理解UriMatcher

http://developer.android.com/reference/android/database/Cursor.html

此链接帮助您从内容提供者或者直接从数据库读取数据

http://www.sqlite.org/sqlite.html

这是SQLite的主页,在改网站中,你可以学习以及下载SQLite工具

http://www.androidbook.com/projects

使用以上地址可以下载本章的相关代码。文件名称为ProAndroid3_ch04_TestProvider.zip

 

总结

在本章,你学习到了URI,MIME类型以及内容提供者。并且学会了如何使用SQLite构建一个通过URI描述的内容提供者。只要遵循内容提供者的规则共享底层数据,任何在Android平台下的应用程序都可以对其进行操作。内容提供者提供了通过URI访问并且更新数据的能力。在下一章,我们会对意图(intent)进行讲解。意图通过data URI和URI MIME类型与内容提供者(包括其它Android组件)绑定。这些本已经学习的知识会有助于您对意图的理解,data URI将扮演重要的角色。