Android使用了一个名为content provider的概念将数据提取到service中。这种使用content provider的想法使得数据源看起来像一个基于REST-enabled的数据提供者,类似于网页。这样,contentn provider就是一个被数据环绕的包装器。Android设备上的SQLite数据库就是一种这样的数据源:你可以将其封装到一个content provider中。
//select all rows from a table
select * from table1;
//count the number of rows in a table
select count(*) from table1;
//select a specific set of columns
select col1, col2 from table1;
//Select distinct values in a column
select distinct col1 from table1;
//counting the distinct values
select count(col1) from (select distinct col1 from table1);
//group by
select count(*), col1 from table1 group by col1;
//regular inner join
select * from table1 t1, table2 t2
where t1.col1 = t2.col1;
//left outer join
//Give me everything in t1 even though there are no rows in t2
select * from table t1 left outer join table2 t2
on t1.col1 = t2.col1
where ....
content://contacts/people/
content://contacts/people/23
video/x-msvideo
ContactsContract.Contacts.CONTENT_URI
...
// An array specifying which columns to return.
string[] projection = new string[] {
Contacts._ID,
Contacts.DISPLAY_NAME_PRIMARY
};
Uri mContactsUri = ContactsContract.Contacts.CONTENT_URI;
// Best way to retrieve a query; returns a managed query.
Cursor managedCursor = managedQuery( mContactsUri,
projection, //Which columns to return.
null, // WHERE clause
Contacts.DISPLAY_NAME_PRIMARY + " ASC"); // Order-by clause.
managedQuery方法中where的参数为空是因为本例中我们假设note provider足够聪明,可以找到我们指定的id。这个id已经嵌入到了URI中。我们使用URI作为载体传递where语句。看一下这个notes provider如何实现查询语句,就可以理解这么做的原因了。下面是一段查询代码:
values.put("note","This is a new note");
//Use a content resolver to insert the record
ContentResolver contentResolver = activity.getContentResolver();
Uri newUri = contentResolver.insert(Notepad.Notes.CONTENT_URI, values);
//ContentResolver hides the access to the _data field where
//it stores the real file reference.
OutputStream outStream = activity.getContentResolver().openOutputStream(newUri);
someSourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
......
case INCOMING_SINGLE_BOOK_URI_INDICATOR:
......
default:
throw new IllegalArgumentException("Unknown URI " + uri);
sUriMatcher.addURI(BookProviderMetaData.AUTHORITY
, "books"
, INCOMING_BOOK_COLLECTION_URI_INDICATOR);
sUriMatcher.addURI(BookProviderMetaData.AUTHORITY
, "books/#",
INCOMING_SINGLE_BOOK_URI_INDICATOR);
}
{
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);
}
{
String tag = "Exercise BookProvider";
int i = getCount(context); //See the getCount function in Listing 4–11
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 = 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;
}
Listing 4–12. Displaying a List of Books
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);
//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();
Log.d(tag,"Num of Records:" + numberOfRecords);
//Close the cursor
//ideally this should be done in
//a finally block.
c.close();
注:REST表示REpresentation State Transfer。当你在浏览器中输入一个url时,网络服务器返回一个html,你其实就是在web服务器端进行了一次基于REST的“查询”操作。REST经常与SOAP(simple object access protocal)网络服务进行比较。你可以从下面的维基百科网站了解到更多的关于REST的内容:
要想从content provider获取数据,或者将数据保存在content provider中,你需要一些类似REST的URIs.例如:你想要从一个封装有book数据库的content provider中获取一系列书籍,你需要一个类似于下面例子的URI:
content://com.androidbook.book.BookProvider/books/23
在本章,你将会看到这些URIs如何转变为更底层的数据库访问机制。设备上任何应用都可以使用这些URIs来访问和操作数据。因此,content provider在应用程序之间共享数据中扮演了十分重要的角色。
严格地将,content provider的主要职责不仅仅是提供了一种获取数据的机制。你需要一个真正的数据获取机制如SQLite或网络访问来获取底层数据。所以,content provider这种抽象仅仅在你需要在外部或应用程序之间共享数据时使用。对于内部数据访问,可以使用storage/access机制更为合适,如:
Preferences:用一系列键值对来长久的存储应用偏好。
Files:应用的内部文件,你可以将其存储到可移除的存储媒介上。
SQLite:SQLite数据库,每个数据库仅对创建该数据库的包可见。
Network:一种可以通过HTTP服务来在外部存储数据的机制。
注:暂且不管提到的各种存储方式,本章重点关注SQLite和content provider抽象。因为相较于其它的UI框架,Android框架对于content provider的使用十分普遍。我们将在第11章关注Network存储,在第9章介绍Preferences机制。
浏览一下Android内建的Provider
Android有一系列的内建content provider,在SDK的android.provider的java包中有详细的文档说明。你可以从下面网址浏览包含的content provider:
http://developer.android.com/reference/android/provider/package-summary.html
例如:provider包括Contacts和Media Store。这些SQLite数据库以.db为扩展名,仅在创建其的包内可见。任何包外的访问,必须经过content provider接口进行。
浏览一下模拟器和设备上的数据库
因为Android上的很多content provider都使用了SQLite数据库(www.sqlite.org),所以你可以使用Android或SQLite提供的工具检查数据库。许多这样的工具位于\android-sdk-install- directory\tools子目录下,其它的工具位于 \android-sdk-install-directory\platform-tools目录下。
注:请参考第2章介绍的关于工具位置以及在不通过的操作系统下启动命令行窗口的方法。本章,以及以后绝大多数章节给出的例子均是基于Windows操作系统。本章中我们使用了很多命令行工具,你只需要注意各个可执行文件或批处理文件的名称,不必关注其具体路径。我们在第二章已经介绍了如何在不同操作系统下设置工具路径。
Android使用了一个名为adb(Android Debug Bridge)的命令行工具。其路径如下:
platform-tools\adb.exe
adb工具比其它大多数与设备通信的工具都要特殊。然而,你必须有一个模拟器或者android真机连接才能使adb正常工作。你可以通过下面的命令查看是否有正在运行的设备或者模拟器:
adb devices
如果模拟器没有运行,你可以通过下面命令启动模拟器:
emulator.exe @avdname
其中@avdname参数是android虚拟机(AVD)的名字。(第二章介绍了avd所需内容以及如何创建avd)。你可以执行下面的命令查看那些虚拟机已经存在:
android list avd
这个命令将会列出可用的AVDs。如果你已经通过ADT(Android Development Tool)开发并运行过任意应用程序,那么你已经配置过至少一个虚拟机了。前面的命令将会至少列出其中一个。
下面是这个命令的一个输出实例。(其内容取决于你的工具路径以及Android版本,下面的例子有可能由于路径和发布版本不同而不同,如i:\android)
正如我们提到的,关于AVD的细节在第2章可以看到。
你也可以通过Eclipse的ADT插件来启动虚拟机。当你选择某个程序运行或者调试时,会自动启动虚拟机。当虚拟机已经准备好并开始运行,你可以再次测试一下可运行虚拟机列表:
adb devices
现在你会看到如下输出:
你可以通过下面的命令查看adb更多选项和命令:
adb help
你也可以通过下面链接查看关于adb的更多运行时选项:
你也可通过adb打开一个连接设备的shell:
adb shell
注:shell是一个Unix ash,尽管只有一些受限的命令集。例如你可以使用ls,但是find、grep和awk不能再这个shell中使用。
你可以在shell输入窗中输入下面命令来查看这个shell所支持的命令:
#ls /system/bin
"#"号是shell的提示符。为简便起见,我们在下面的例子里将省略这个提示符。想要看根目录下的路径可以用下面的命令:
ls -l
你可以通过下面的命令查看数据库列表:
ls /data/data
这个命令列出了设备已安装的包。我们以com.android.providers.contacts包为例:
ls /data/data/com.android.providers.contacts/databases
该命令会列出一个名为contacts.db的文件,这是一个SQLite数据库。(这个文件依然依赖于设备和发布版本)
注:在Android中,数据库有可能仅在第一次访问时才会创建。这表明如果你从未打开过contacts应用,你将不会看到这个文件。
如果在这个ash里面有个find命令,那么你就可以看到所有的*.db文件。但是只有ls无法做到这点。你能做到的最接近的方法是:
ls -R /data/data/*/databases
通过这个命令,你将会看到这个Android版本有下面几个数据库(再一次提醒,下面的数据库可能由于你的版本不同而不同):
alarms.db
contacts.db
downloads.db
internal.db
settings.db
mmssms.db
telephony.db
你可以在adb里输入下面命令唤醒sqlite3:
sqlite3 /data/data/com.android.providers.contacts/databases/contacts.db
你可以通过下面命令退出sqlite3:
sqlite3>.exit
请注意adb的提示符为#,而sqlite3的提示符为sqlite>。你可以通过 www.sqlite.org/sqlite.html 来了解更多sqlite3的命令。不过,我们下面会列出一些重要的命令,这样你就不必再去网站查询。你可以通过下面的命令列出所有table。
sqlite>.tables
这个命令式下面命令的简写:
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获取表中列的一种方法。这种方法还会打印出数据类型。当使用content provider时,你应该记住这些数据类型,因为获取数据的方法依赖于数据类型。
然而,如果是仅仅想知道列名和数据类型,分析这样复杂的创建状态还是令人烦躁的。有一个变通方案,那就是你可以把这个数据库拖到本地。然后用支持SQLite3可视化的工具来查看。你可以使用下面的命令来拖出数据库:
adb pull /data/data/com.android.providers.contacts/databases/contacts.db c:/somelocaldir/contacts.db
我们使用一个可以免费下载的查看SQLite数据库的可视化工具Sqliteman(http://sqliteman.com/)。我们虽然碰到过几次崩溃,但是总体来说还是比较适合查看Android的SQLite数据库的。
SQLite快速指南
下面的SQLite命令可以帮助你快速浏览一个数据库:
//Set the column headers to show in the tool
sqlite>.headers on//select all rows from a table
select * from table1;
//count the number of rows in a table
select count(*) from table1;
//select a specific set of columns
select col1, col2 from table1;
//Select distinct values in a column
select distinct col1 from table1;
//counting the distinct values
select count(col1) from (select distinct col1 from table1);
//group by
select count(*), col1 from table1 group by col1;
//regular inner join
select * from table1 t1, table2 t2
where t1.col1 = t2.col1;
//left outer join
//Give me everything in t1 even though there are no rows in t2
select * from table t1 left outer join table2 t2
on t1.col1 = t2.col1
where ....
Content Providers的架构
你现在已经知道如何通过Android和SQLite工具查看已经存在的content providers。接下来我们将研究一下content providers的结构,以及content providers是如何连接到数据库抽象中的。
总的来说,content provider的方法与下面业内的抽象是平行的:
Web Sites
REST
Web Services
Stored Procedures
设备上的每一个content provider都通过一个字符串(类似于域名,不过被称为authority)将自己注册到设备上,类似于一个网页。这个独一无二的标识是一个content provider能够支持的所有URIs的基础。这与一个网页通过一个域名提供一系列的URLs来链接到自己的文档或内容并无两样。
在AndroidManifest.xml中注册Authority。下面是两个在AndroidManifest.xml文件中注册providers的例子:
<provider android:name="SomeProvider" android:authorites="com.your-company.SomeProvider"/>
<provider android:name="NotePadProvider" android:authorities="com.google.provider.NotePad"/>
Auhtority就是一个provider的域名。Content provider会默认URLs都以之前注册的authority为前缀:
content://com.your-company.SomeProvider/
content://com.google.provider.NotePad/
content://com.google.provider.NotePad/
你可以看到content provider与网页类似,都有一个基础域名作为URL的开始部分。
注:Android内部的content providers很可能没有使用规范的authority。不过对于第三方的content providers还是推荐使用规范的authority。这就是为什么你会看到有时content provider引用了一个简单的单词如contacts而不是com.google.proveder.Contacts(第三方provider使用)。
Content Providers还提供了一种类似REST的URLs来获取或者操作数据。通过前面注册的authority,在NotePadProvider数据库里定位到定位到所有notes的URI为:
content://com.google.provider.NotePad/Notes
而定位某个具体的note的方法是:
content://com.google.provider.NotePad/Notes/#
其中#代表某个具体的note的id,下面还有一些Content Provider可以接受的URIs的例子:
content://media/internal/images
content://media/external/imagescontent://contacts/people/
content://contacts/people/23
注意这些provider的media(content://media)和contacts(content://contacts)并没有使用标准的authority结构。因为这并不是第三方的providers,而是由Android控制的。
Content Provider还具有Web Services的特性。一个content provider通过URIs将自己内部的数据暴露给外界,这与service类似。然而,content provider的输出并不像基于SOAP的网络服务器一样是一个特定类型的数据。其输出更像是一个JDBC的输出。虽然与JDBC类似,但仅仅是概念上的类似。我们并不想让你认为它与JDBC的ResultSet相同。
调用者是很想知道输出的行列结构的。同时,正如你将要在本章的“Android的MIME类型结构(Structure of Android MIME Types)”一节中看到,content provider有一个内建的机制来保证由你通过URI来决定输出的MIME(Multipurpose Internet Mail Extensions)类型。
不仅仅像Web Site, REST和Web Services,content provider还有Stored procedures类似。Stored procedures通过基于service的查询来访问底层相关数据。URIs与stored precedures类似,因为URI可以使content provider返回一个cursor。然而,两者之间也有不同,前者对service的调用已经嵌在了URI之内。
我们通过上述对比,从更广的角度给你一个关于content provider认识。
Android Content URIs的结构
我们将content provider与网页作比较是因为其也是对URIs做出响应。所以从content provider获取数据,你所需要做的全部事情就是使用一个URI。这样,从content provider获取的数据通过Android的cursor对象返回,其内容包括一系列行和列。在这种情况下,我们需要研究一下用来获取数据的URI的结构。
Android中的content URIs与HTTP的URIs类似,除了其以content开头,并且具有下面的形式:
content://*/*/*
或者:
content://authority-name/path-segment1/path-segment2/etc…
下面是一个例子,表示note数据库中第23个note:
content://com.google.provider.NotePad/notes/23
在content之后就是一个独一无二的authority,用来在provider的注册表中定位provider。前面的例子中:com.google.provider.NotePad就是URI的authority部分。
/notes/23 是这个URI的路径部分,每个provider的路径不尽相同。“notes”和“23”被称为path segments。provider负责说明并解释URIs的path section 和path segments。
content provider的开发者通常会在其所在的java包中通过Java类的常量或接口来声明URI的path section。进一步讲,path的第一部分通常会指向对象的集合。例如:/notes表示一系列notes集合的路径,而/23表示其中的一个note元素。
通过给定的URIs,content provider希望可以获取到该URI所指的行。还希望能够改变该URI指向的内容,如insert、update或者delete。
Android MIME类型结构
与网页为一个给定的URL返回一个MIME类型一样(这允许浏览器调用正确的应用来浏览内容),content provider也有一个任务,就是返回给定URI的MIME类型。这为浏览数据提供了弹性。知道了数据类型,你可能有不止一个应用来处理数据。例如,你的硬盘中有个txt文件,可以由多个编辑器来显示该文件。依赖于不同的操作系统,很有可能推荐不同的编辑器供你选择。
Android中的MIME类型与HTTP中的工作方式相同。你向provider索取一个它支持的URI的MIME类型时,它会返回一个由两部分字符串组成的MIME类型,这与标准的web的MIME类型相同。你可以从下面网址查看MIME类型标准:
根据MIME类型说明,一个MIME类型包含两个部分:类型和子类型。下面列举一些常见的MIME类型:
text/html
text/css
text/xml
text/vnd.curl
application/pdf
application/rtf
application/vnd.ms-excel
你可以从下面Internet Assigned Numbers Authority (IANA)网址中查到所有已注册的MIME的类型和子类型:
主要的注册类型如下:
application
audio
example
image
message
model
multipart
text
video
每个主类型都有一些子类型。如果第三方有合适的数据格式,子类型以vnd开头。例如微软的excel电子表格通过vnd.ms-excel来标识,而pdf被认为非第三方的格式,因此没有vnd前缀。
一些子类型以x-作为前缀。这是非标准的子类型,无需注册。它们被视为由两个组织定义的私有值。下面是一些例子:
application/x-tar
audio/x-aiffvideo/x-msvideo
Android遵循了相同的方式来定义MIME类型。vnd在Android MIME类型中被视为非标准的,第三方的类型。为了提供特殊性,Android将主类型和子类型划分为多个部分,类似于域名那样定义。更进一步,对于每一个内容类型,Android的MIME类型 有两种形式:一个针对单条记录,一个针对多条记录。
对于单条记录,MIME类型类似于:
vnd.android.cursor.item/vnd.yourcompanyname.contenttype
而对于多个row的集合,MIME类型为:
vnd.android.cursor.dir/vnd.yourcompanyname.contenttype
下面是两个例子:
//One single note
vnd.android.cursor.item /vnd.google.note
//A collection or a directory of notes
vnd.android.cursor.dir/vnd.google.note
vnd.android.cursor.item /vnd.google.note
//A collection or a directory of notes
vnd.android.cursor.dir/vnd.google.note
注:其影响是Android本地可以识别多个条目的路径和单个条目。而作为一个程序编写者,你的弹性仅限于子类型。例如:列表控制就依赖于cursor返回的内容来作为其主类型。
MIME类型在Android中广泛应用,特别是在intent中,系统通过基于MIME类型的数据来唤醒对应的activity。MIME类型通过content provider,从URI中一成不变的继承下来。当你使用MIME类型时需要记住三点:
类型和子类型是唯一的。正如前面指出,主类型已经为你指定好了。它主要是一系列条目的路径或某个条目。在Android中,这一点可能显得并没有你想象的那样开放。
如果类型和子类型为非标准的,那么需要用vnd作为前缀。(当你涉及到某个特定的记录时常常这样做)
对于你特定的需求,它们拥有典型的命名空间。
再次重申,通过Android的cursor返回的多个条目集合的主MIME类型必须是vnd.android.cursor.dir,而返回的单个条目的主MIME类型必须是vnd.android.cursor.item。对于子类型,你又有更多的修改空间,如vnd.google.note。在vnd之后,你可以使用任意内容作为子类型。
通过URIs读取数据
你现在已经知道,如果想从content provider获取数据,你需要使用provider提供的URIs。因为不同的content provider的URIs是不同的,因此content provider应该提供URI的文档让编程者使用。Android自带的content provider通过定义常量来表示这些URIs字符串。
考虑一下Android的SDK的helper类中定义的这三个URIs:
MediaStore.Images.Media.INTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI ContactsContract.Contacts.CONTENT_URI
其对应的URI字符串为:
content://media/internal/images
content://media/external/images
content://com.android.contacts/contacts/
content://media/external/images
content://com.android.contacts/contacts/
通过这些URIs,如果想从Contacts provider中获取某人的一行数据的方式如下:
Uri peopleBaseUri = ContactsContract.Contacts.CONTACT_URI;
Uri myPersonUri = Uri.withAppendedPath(peopleBaseUri, "23");
// Query for this record
//managedQuery is a method on Activity class
Cursor cur = managedQuery(myPersonUri, null, null, null);
Cursor cur = managedQuery(myPersonUri, null, null, null);
注意一下ContactsContract.Contacts.CONTENT_URI在Contacts类里是如何定义的。我们将变量命名为peopleBaseUri,表示如果你想要查询people,就可以在这个联系人的URI后面追加内容。当然如果你认为people就是contacts的话,你也可以将其命名为contactBaseUri。
注:更多关于Contacts provider的信息可以参考第30章。还应该注意Contacts的API及其相关的contacts可能随版本而不同。本章在Android 2.2(API 8)及以上版本进行测试。
本例中,代码使用该URI作为根URI,并在后面加上了一个特定的people ID,然后调用了managedQuery方法。
作为这个URI查询的一部分,还可以指定排序方式,选择的列名和where语句。本例中这些参数被设置为null。
注:一个content provider通过实现一系列接口,后者指定列名来列出其所支持的列。然而,定义contacts列的接口或类需要通过列名转换、注释或文档说明来声明一个列的类型,这是因为并没有一个官方的形式来指定列的类型。
Listing 4-1介绍了如何查询一个联系人content provider返回的包含特定列的cursor:
//Use this interface to see the constants
import ContactsContract.Contacts;...
// An array specifying which columns to return.
string[] projection = new string[] {
Contacts._ID,
Contacts.DISPLAY_NAME_PRIMARY
};
Uri mContactsUri = ContactsContract.Contacts.CONTENT_URI;
// Best way to retrieve a query; returns a managed query.
Cursor managedCursor = managedQuery( mContactsUri,
projection, //Which columns to return.
null, // WHERE clause
Contacts.DISPLAY_NAME_PRIMARY + " ASC"); // Order-by clause.
注意:projection仅仅是一个列出列名的字符串数组。所以,除非你知道其列名,否则你很难构造一个projection。你应该在提供该URI的相同类中查询都有哪些列名,本例中就在Contacts中查找。你可用通过查看android的SDK中关于android.provider.ContactsContract.Contacts类的文档找到每个列的信息:
http://developer.android.com/reference/android/provider/ContactsContract.Contacts.html
我们再次复习一下这个返回的cursor:它包含0到多条记录。列名、顺序和类型时provider指定的。不过,每个返回的行中都有一个名为_id的列来唯一标识这一行。
使用Android的Cursor
下面是Android的Cursor的一些情况:
Cursor就是一系列行的集合。
在读取数据之前,你需要使用moveToFirst()方法,因为最开始cursor位于第一行之前。
你需要知道列名。
你需要知道列的类型。
所有查询域的方法都基于列的号码,所以你首先需要将列名转换为列号。
cursor很灵活,你可以向前、向后,也可以跳跃查询。
因为cursor是随机的,所以你可以通过它查询行数。
Android 的cursor有一系列方法让你查询。Listing4-2告诉你如何判断一个cursor是否为空,如果不为空,如何逐行遍历这个cursor。
该例首先假设cursor已经定位在第一行之前。为了把cursor定位到第一行,我们使用了cursor对象的moveToFirst()方法。如果cursor为空,则该方法返回false。我们然后使用moveToNext()方法来遍历cursor。Listing 4–2. Navigating Through a Cursor Using a while Loopif (cur.moveToFirst() == false){//no rows empty cursorreturn;}
//The cursor is already pointing to the first row//let's access a few columnsint nameColumnIndex = cur.getColumnIndex(Contacts.DISPLAY_NAME_PRIMARY);String name = cur.getString(nameColumnIndex);
//let's now see how we can loop through a cursor
while(cur.moveToNext()){//cursor moved successfully//access fields}
为了帮助你知道cursor的位置,Android提供了下面的方法:
isBeforeFirst()
isAfterLast()
isClosed()
通过这些方法,你可以不使用while来遍历cursor,如Listing4-3所示:
每一列的索引看起来似乎有些随意。因此,我们建议你一开始就来获取列的索引来防止出现异常。Android在cursor对象中提供了一个getCount()方法来获取cursor中的行数。Listing 4–3. Navigating Through a Cursor Using a for Loop//Get your indexes first outside the for loopint nameColumn = cur.getColumnIndex(Contacts.DISPLAY_NAME_PRIMARY);
//Walk the cursor now based on column indexesfor(cur.moveToFirst();!cur.isAfterLast();cur.moveToNext()){String name = cur.getString(nameColumn);}
使用WHERE语句
Content provider提供了两种方法来传递wher语句:
通过URI
通过将一个字符串语句和可替换的字符串数组参数合并
我们将通过一些例子来具体介绍这两种方法。
通过URI传递where语句
假设你想从Google notes数据库中获取id为23的一个note。你需要使用Listing4-4的方法来获取note表中行数为23的数据。
Listing 4–4. Passing SQL where Clauses Through the URI
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 clausenull); // Order-by clause.
managedQuery方法中where的参数为空是因为本例中我们假设note provider足够聪明,可以找到我们指定的id。这个id已经嵌入到了URI中。我们使用URI作为载体传递where语句。看一下这个notes provider如何实现查询语句,就可以理解这么做的原因了。下面是一段查询代码:
//Retrieve a note id from the incoming uri that looks like
//content://.../notes/23int noteId = uri.getPathSegments().get(1);
//ask a query builder to build a query//specify a table namequeryBuilder.setTables(NOTES_TABLE_NAME);
//use the noteid to put a where clausequeryBuilder.appendWhere(Notes._ID + "=" + noteId);
注意一下note的id是如何从URI中获取的。表示输入参数的Uri类有一个方法来提取URI根路径(content://com.google.provider.NotePad )后面的部分。这些部分叫做path segments,它们是“/”分隔符之间的字符串,如/seg1/seg2/seg3/seg4/,而且它们根据其位置进行索引。对于这个URI,第一个path segment就是23。然后我们用这个23的id追加到QueryBuilder类之后。最终,这种方法等价于下面的语句:
select * from notes where _id = 23;
注:类Uri和UriMatcher用来识别URIs并且从中提取参数。(我们将在后面的“使用UriMatcher来识别URIs”章节详细介绍UriMatcher)SQLiteQueryBuilder是androiddatbase.sqlite中的一个帮助类,它能够结构化你的query语句,使SQLiteDatabase类能够执行。
使用显示的where语句
现在你已经知道如何通过Uri来发送一个where语句,现在我们介绍另一个显示的输入where语句的方法。首先我们需要看一下在Listing4-4中使用到的managedQuery方法。 下面是其定义:
public final Cursor managedQuery(Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder)
注意名为selection的参数,其类型为String。这个字符串表示一个过滤器(filter,特别是指where语句)来声明返回那些行。它需要格式化为一个SQL的where语句(需要包含WHERE本身)。如果传入null,则返回URI所指的所有的行。在这个selection字符中,你可以加入多个“?”号,该问号会本selectionArgs参数依此替换,其值应该是字符串类型。
因为你有两种方法来实现where语句,所以你可能会发现如何选择这两种where语句比较困难,另外,如果两个语句同时使用,那么会优先执行那个呢?
例如你可以通过下面方法查询id为23的note记录:
//URI method
managedQuery("content://com.google.provider.NotePad/notes/23" ,null ,null ,null,null);
或者使用下面的方法:
//explicit where clause
managedQuery("content://com.google.provider.NotePad/notes" ,null ,“id=?” ,new String[] {23}, null);
通常情况下,在能使用URIs的地方尽量在URIs里使用where语句,而把显示的使用where语句作为一种特殊情况。
插入记录
现在我们已经知道如何通过URIs获取数据。现在我们开始关注如何插入、更新和删除数据。
注:为了说明content provider,我们使用了google教程中的notepad应用作为原型,但是不必对这个应用非常熟悉。及时你一点都不熟悉这个应用,你也应该能看懂这些例子。不过,在本章稍后,我们会给出一个完成的provider例子的代码。
Android使用一个名为android.content.ContentValues的类来进行单条数据的插入。ContentValues是一个键值对,就像列的名字和列的值一样。在插入记录之前需要先将记录填入ContentValues中,然后借助android.content.ContentResolver通过URIs将其插入。
注:你需要定位ContentResolver,这是因为在这个抽象层次上,你并不是在让一个数据库插入某条数据,而是请求将数据插入由URIs标识的provider中。ContentResolver负责处理与provider相关联的的URI,并吧ContentValues传递到特定的provider中。
下面是一个将单条notes记录填充到ContentValues中的例子:
ContentValues values = new ContentValues();
values.put("title", "New note");
values.put("note", "This is a new note");
这样Values对象就已经准备好了,可以随时插入到provider中。
你可以通过activity来获取ContentResolver的引用:
ContentResolver contentResolver = activity.getContentResolver();
现在你所需要的就是一个URI来告诉ContentResolver把这一行内容插到什么地方。这些URIs定义在与Notes表相关的类中。在这个Notepad例子中,该URI是Notepad.Notes.CONTENT_URI。
我们可以通过这个URI和之前的ContentValues来插入这一行:
Uri uri = contentResolver.insert(Notepad.Notes.CONTENT_URI , values);
该调用返回一个Uri地址,指向新插入的一行。该Uri应该与下面的格式相匹配:
Notepad.Notes.CONTENT_URI/new_id
在Content Provider中加入文件
在某些情况,你可能需要在一个数据库中插入一个文件。通常的做法是把文件存储在磁盘中,然后再数据库中存入相应文件的引用地址。
Android也采用此种协议,并通过定义一个特定的存储和获取文件的步骤来使这个过程自动化。Android遵循了这样一种惯例:将文件名存储在一条记录中,且将其置于预留的名为_data的列中。
当这样一条记录插入表中,Android会返回给调用者一个URI。一旦你使用这种机制来插入一条记录,你也同时需要将文件存储在相应的位置中。为了达到这个目标,Android运行ContentResolver通过返回的URI来得到一个可写的输出流。在这一切的背后,Android创建了一个内部文件,并把该文件的引用存储在_data字段中。
如果你想扩展一下Notepad应用,想在已给的note里插入一张图片。你需要额外建立一个_data列,并且先调用insert方法来获取一个URI.下面的代码就展示了这个过程:
ContentValues values = new ContentValues();
values.put("title", "New note");values.put("note","This is a new note");
//Use a content resolver to insert the record
ContentResolver contentResolver = activity.getContentResolver();
Uri newUri = contentResolver.insert(Notepad.Notes.CONTENT_URI, values);
一旦你得到某条记录的URI,下面的代码可以获取到一个输出流的引用:
....
//Use the content resolver to get an output stream directly//ContentResolver hides the access to the _data field where
//it stores the real file reference.
OutputStream outStream = activity.getContentResolver().openOutputStream(newUri);
someSourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream);
outStream.close();
最后,代码通过输出流将文件写入。
更新和删除
现在我们已经学习了查询和插入,更新和删除就比较直接了当了。
{
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";
{
//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;
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.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;
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)) {
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;
}
进行更新和插入类似,发生变化的列通过ContentValues对象进行传递。下面就是一例:
int numOfRowUpdate = activity.getContentResolver().update(Uri uri, ContentValues values, String whereClause, String[] selectionArgs);
whereClause参数限制了更新特定的相关行。类似的,删除的语法如下:
int numOfRowDelete = activity.getContentResolver().delete(Uri uri, String whereClause, String[] selectionArgs);
很明显,删除并不需要ContentValues,因为删除操作并不需要指定特定的列。
差不多manageQuery、ContentResolver等对象的所有调用都最终指向provider类。如果知道Content Provider是如何实现这些方法,那么就会对用户了解如何使用这些方法提供一定的线索。下一节我们将通过一个名为BookProvider的例子来剖析一下Content Provider的具体实现。
实现Content Provider
我们已经讨论了如何为了数据需要来与content provider进行通信,但是还没有介绍如何实现一个Content Provider。为了实现一个Content Provider,你必须继承android.content.ContentProvider类,并实现下面几个方法:
query
insert
update
delete
getType
在实现这些方法之前,你还需要创建一系列内容。我们会通过介绍下面几个步骤来充分说明如何实现一个Content Provider:
1、为你的数据库、URIs、列名等内容作规划,另外创建一个元数据类来定义所需的所有常量。
2、继承ContentProvider类。
3、实现query,insert,update,delete,getType方法
4、在manifest文件中注册provider。
规划数据库
为了扩展这一话题,我们需要建一个关于Books的数据库。该数据库只有一个表,名为books,其列包括:name,isbn和author。这些列名收元数据的影响。你需要在一个类中定义这些元数据。包含这些元数据的类名为BookProviderMetadata,具体内容如Listing 4-5所示。一些主要的元数据已经高亮表示。
Listing 4–5. Defining Metadata for Your Database: The BookProviderMetaData Class
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";
}
}
//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";
}
}
这个类首先定义了authority:com.androidbook.provider.BookProvider.我们将利用这个字符串在manifest里注册provider。这个字符串构成了指向provider的URIs的前半部分。
这个类然后通过一个名为BookTableMetaData的内部类来定义一个books表。该内部类定义了一个URI用来指向books集合。通过使用前面定义的authority,这个URI就成为:
content://com.androidbook.provider.BookProvider/books
这个URI通过下面的常量表示:
BookProviderMetaData.BookTableMetaData.CONTENT_URI
BookTableMetaData类然后分别为books集合和单个book定义了MIME类型。provider实现将会使用这两个常量来为传入到的URI返回MIME类型。
BookTableMetaData又定义了列名:name, isbn, author, created(创建时间), modified(上次修改时间)。
注:你应该根据代码中的注释定义数据类型。
BookTableMetaData继承了BaseColumns类,该类提供了一个标准域_id,代表一个行的id。有了这些元数据,我们就可以继续完成provider的实现了。
继承ContentProvider
为了实现我们的BookProvider,我们需要继承ContentProvider类,并重写onCreate()方法来创建数据库,并且需要实现query、insert、delete、update和getType等方法。这一部分包含数据库的创建,而接下来的部分包含上述方法的实现。Listing4-6列出了完整的源代码,其中重点部分已经高亮显示。
query方法需要知道要返回的列的集合。这与select语句需要列名一样。Android通过使用名为projection的map来表示这些列名和及其别名(synonyms)。我们需要建立起这种映射,这样就可以在实现query方法中用到。下面代码中,你会看到这些会作为Project映射创建的一部分先建立起来。
我们所需要实现的大部分方法都将一个URI作为输入。尽管可能content provider所响应的URIs都是以相同的内容作为开始,但是结尾部分并不相同,正如网页一样。对于每一个URI,尽管开始部分相同,但是整体必须不同,以便区分不同的数据和文档。下面是一个例子:
Uri1: content://com.androidbook.provider.BookProvider/books
Uri2: content://com.androidbook.provider.BookProvider/books/12
本例中book provider需要区分这些URIs。这是一个很简单的事情。如果我们的book provider需要包含更多的内容而不仅仅是books,那么就需要更多的URIs进行区分这些对象。
provider的实现需要某种机制来区分这些URIs。Android通过一个名为UriMatcher的类来实现这一目的。你在Lisitng 4-6中创建projection映射之后的代码中可以找到相关内容。我们在”使用UriMatcher区分URIs“一节中会进一步解释UriMatcher。
Listing 4-6中的代码重写了onCreate方法来完成数据库的创建。然后完成了insert(), query(), update(), getType()和delete()等方法的实现。所有的代码都放在同一个列表中,不过我们会在不同的章节中进行解释。
Listing 4–6. Implementing the BookPr ovider Content Provider
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);
}
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 (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;
}
}
完善MIME类型Fulfilling MIME-Type Contracts
bookprovider必须实现getType()方法以便为指定的URI放回MIME类型。这个方法与content provider其他的方法一样,根据不同的URIs进行重载。因此,getType()方法的第一个任务就是区分不同的URIs类型。它到底是一个books的集合和一个单一的book。
正如我们前面章节所说,我们需要用一个UriMatcher来解析URI类型。基于该URI,BookTableMetaData类定义了为每一种URI返回的类型。你可以在Listing4-6中看到该实现。
实现query方法
Content Provider中的query方法负责依据URIs及where语句返回特定的行的集合。
与其他方法一样,query方法通过UriMatcher来确定URI的类型。如果是一个单项目的类型,那么该方法通过下面的方法来获取单个book的id:
1、通过getPahtSegments()方法来获取路径部分。
2、其索引到URI来获取第一个路径片段(path segment),而该片段恰好是bood的ID。
query方法然后通过前面已经创建的projections来确定返回那些列。最后,query方法返回给调用者一个cursor对象。整个处理过程,query方法都使用SQLiteQueryBuilder对象来格式化并执行query命令。(见Listing 4-6)
实现Insert方法
Content provider中的insert方法负责将一条记录插入到后台的数据库中,然后返回新插入记录的URI。
与其他方法一样,insert方法使用UriMatcher来确定URI的类型。代码首先要检查URI是否为集合类型,如果不是则抛出异常。(见Listing 4-6)
代码然后检查可选参数和必选参数的合法性。如果某些列为指定,代码中可以赋予其默认值。
接下来,代码通过SQLiteDatabase对象将记录插入到数据库并返回该条记录的id。最后,代码通过返回的id重新组装URI。
实现update方法
Content Provider中的update方法负责根据传入的列的值以及where语句对数据库中的某一条或多条记录进行更新。该方法,返回最终更新的记录行数。
与其他方法一样,update方法使用UriMatcher来确定URI的类型。如果是集合类型,则传入where语句,以便能影响尽可能多的符合条件的记录。如果是一个单记录类型,book的id将会从URI中解析出来作为where语句。最后代码返回发生改变的记录数目。(见Listing 4-6)同时需要注意,如何使用notifyChange方法来对外广播该URI所指向的数据已经发生改变。同样,你也可以在insert方法中声明当插入记录时".../books"已经发生改变。
实现delete方法
Content Provider中的delete方法负责根据传入的where语句来删除某一条或多条记录,然后返回所删除记录的数目。
与其他方法一样,delete方法使用UriMatcher来确定URI的类型。如果是集合类型的URI,则where语句可以使尽可能多的符合条件的记录被删除。如果where语句为null,则所有的记录都将被删除。如果URI是单记录类型,book的id将会从URI中解析出来作为where语句。最后,该方法返回删除的记录数目。(见Listing 4-6)
使用UriMatcher区分URI类型
现在我们已经多次提及UriMatcher了,让我们进一步对其进行分析。几乎所有的方法都要根据URI的类型进行重载。例如,无论你是想查询单个book还是一个books集合,都会调用同一个query方法。由query方法来确定是哪种类型的URI。Android的UriMatcher工具类来帮助你区分URI的类型。
其工作原理如下:你首先告诉UriMatcher的实例你需要区分什么类型的URI。你需要将每个类型对应一个独特的数字。一旦这些类型被注册成功,你就可以通过Urimatcher来确定输入的URI是否与某个特定的类型相匹配。
正如我们所说的,我们的BookProvider有两种类型的URI形式:一个是多个books的集合,一个是单个book。Listing 4-7中代码通过使用UriMatcher注册了这两种类型。其中,1对应books的集合,2对应单个book。(URI的格式定义在books表中的元数据中)
Listing 4–7. Registering URI Patterns with UriMatcher
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);
}
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);
}
现在已经注册完毕,你可以在query方法中看到UriMatcher如何发挥其作用:
switch (sUriMatcher.match(uri)) {
case INCOMING_BOOK_COLLECTION_URI_INDICATOR:......
case INCOMING_SINGLE_BOOK_URI_INDICATOR:
......
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
请注意一下UriMatcher是如何返回之前注册的数字的。UriMatcher的构造函数需要一个整数来追加到根URI上。如果既没有path segments也没用authorities,那么UriMatcher返回该整数。当类型不相匹配的时候,UriMatcher会返回NO_MATCH。你也可以不使用根URI来构造一个UriMatcher。这样,Android在内部将UriMatcher初始化为NO_MATCH。所有你可以像Listing4-7中一样:
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映射
Content Provider就像是抽象的列集合和数据库中具体的列集合直接的桥梁,当然这两种集合可能并不相同。当执行查询操作时,你必须在用户通过where语句指定的列和实际数据库中的列建立起映射关系。你可以借由SQLiteQueryBuilder类建立这种projection映射。
下面是Android SDK文档中关于类QueryBuilder中setProjectionMap方法的说明:
为查询操作创建projection映射。该映射将用户传入到query方法中的列名和实际数据库中的列名建立起映射关系。这对重命名列名和消除连接(joins)时的二义性很有帮助。例如你可以将"name"映射到"people.name"。一旦一个projection映射建立,你必须包含用户可能请求的所有列名,即使键和值可能是相同的。
下面是我们的BookProvider如何建立projection映射的例子:
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);
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);
然后query builder这样使用变量sBooksProjectionMap :
queryBuilder.setTables(BookTableMetaData.TABLE_NAME);
queryBuilder.setProjectionMap(sBooksProjectionMap);
queryBuilder.setProjectionMap(sBooksProjectionMap);
注册Provider
最后,你必须在Android.Manifest.xml中通过下面的标签结构来注册provider。如Listing4-8所示:
Listing 4–8. Registering a Provider
<provider android:name=".BookProvider"
android:authorities="com.androidbook.provider.BookProvider"/>
<provider android:name=".BookProvider"
android:authorities="com.androidbook.provider.BookProvider"/>
使用Book Provider
现在我们已经创建好一个provider了,下面我们将告诉你如何使用它。实例代码包含:增加一个book,移除一个book,获取所有books的数量以及如何显示所有的books。
请注意,实例代码是例子工程中的一部分,并不完整,因为还需要其它依赖的文件。不过,我们还是认为这个例子对于理解我们学到的概念很有帮助。
本章的结束部分有一个示例工程代码的下载链接,你可以下载后再Eclipse中进行运行、测试。
增加一个book
Listing 4-9中的代码展示如何将一个book插入到数据库中。
Listing 4–9. Exercising a Provider Insert
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
Listing4-10中的代码移除了数据库中最后一条book记录。可以通过Listing 4-11中的代码看到getCount()方法如何在Listing 4-10中起作用。
Listing 4–10. Exercising a Provider delete
public void removeBook(Context context){
String tag = "Exercise BookProvider";
int i = getCount(context); //See the getCount function in Listing 4–11
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的方法并非在所有情况下都适用。不过,当你想插入5条数据后,再从末尾逐条删除数据时,这应该可以正常工作。 在实际应用中,你可能要在一个列表中展示所有记录,并且让用户选取某条记录进行删除,这样你就需要知道某条记录所对应的确切的URI了。
获取books的数目
Listing4-11展示了如何获取数据库的cursor,已经通过cursor来获取记录的数目。
Listing 4–11. Counting the Records in a Table
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;
}
显示Books列表
Listing4-12展示如何获从数据库中取所有的记录:
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();
Log.d(tag,"Num of Records:" + numberOfRecords);
//Close the cursor
//ideally this should be done in
//a finally block.
c.close();
}
资源
下面的网址链接可以帮助你更好的理解本章的内容:
http://developer.android.com/guide/topics/providers/content-providers.html: 关于content providers的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: 帮助你直接从content provider或数据库中读取数据。www.sqlite.org/sqlite.html: SQLite主页, 你可以获取更多相关知识并下载工具。
androidbook.com/proandroid4/projects: 从该网址下载示例工程,其压缩文件名称为ProAndroid4_Ch04_TestProvider.zip .。
总结:
本章你学习了如下内容:
什么是Content Providers
如何获取已经存在的content-provider相关联的的数据库
URIs、MIME类型和content provider的特性是什么
如何使用SQLite创建content provder,并对URIs做出相应。
如何在不同进程的不同应用直接共享数据。
如何写一个新的content provider
如何访问一个content provider
如何使用UriMatcher来重载content provider的实现
复习问题
下面的问题可以巩固你对content provider的理解
1、content provider与网页有何相似之处?
2、你能列出几个内置的content provider吗?
3、通过adb工具你可以做些什么?
4、什么是AVD?
5、你如何列出可用的AVDs?
6、Android中的一些有用的命令行工具名称是什么?
7、content provider对应的数据库在什么位置?
8、浏览一个数据库的好的方法是什么?
9、Content provider的authority属性是什么?
10、content provider的authority可以缩写吗?
11、MIME类型是什么?其如何与content provider相联系?
12、程序员如何找到与某content provider通信的URIs?
13、如何通过URIs来访问数据?
14、如何为content provider的query语句传入where语句?
15、如何遍历一个cursor?
16、Content Values扮演一个什么角色?
17、ContentResolver类扮演什么角色?
18、在Content Provider中存储一个文件的协议是什么?
19、UriMatcher如何工作,如何使用UriMatcher?
作者:tanqiantot 发表于2013-8-28 18:24:13 原文链接
阅读:70 评论:0 查看评论