一 Content Provider组件概述
在Android系统中,Content Provider作为应用程序四大组件之一,它起到在应用程序之间共享数据的作用,同时,它还是标准的数据访问接口。
在Android系统中,每一个应用程序被安装时,都会分配到一个不同的Linux用户ID。这样,Android系统就可以基于Linux用户ID来保护每一个应用程序的数据不会被其他应用程序破坏。即每一个应用程序只可以访问自己创建的数据。Android系统是基于Linux内核开发的,而在Linux系统中,每一个文件除了文件本身的数据之外,还具有一定的属性,其中最重要的就是文件权限了。所谓的文件权限,就是指对文件的读写和执行权限,此外,Linux还是一个多用户的操作系统,因此,Linux将系统中的每一个文件都与一个用户以及用户组关联起来,基于不同的用户赋予不同的用户权限。只有当一个用户对某个文件拥有相应的权限时,才能执行相应的操作,例如,只有当一个用户对某个文件拥有读权限时,这个用户才可以调用read系统调用来读取这个文件的内容。Android系统继承了Linux系统管理文件的方法,为每一个应用程序分配一个独立的用户ID和用户组ID,而由这个应用程序创建出来的数据文件就赋予相应的用户以及用户组读写的权限,其余用户则无权对该文件进行读写。
例如
adb shell
//进入到Android系统日历应用程序数据目录
cd data/data/com.android.providers.calendar/databases
m282a:/data/data/com.android.providers.calendar/databases # ls -l
ls -l
total 296
-rw-rw---- 1 u0_a2 u0_a2 122880 2019-04-09 16:17 calendar.db
在前面的十字符-rw-rw----中,最前面的符号-表示这是一个普通文件,接下来的三个字符rw-表示这个文件的所有者对这个文件可读可写不可执行,再接下来的三个字符rw-表示这个文件的所有者所在的用户组的用户对这个文件可读可写不可执行,而最后的三个字符—表示其它的用户对这个文件不可读写也不可执行,因为这是一个数据文件,所认所有用户都不可以执行它是正确的。
在接下来的两个u0_a2字符串表示这个文件的所有者和这个所有者所在的用户组的名称均为u0_a2,这是应用程序在安装的时候系统分配的,在不同的系统上,这个字符串可能是不一样,不过它所表示的意义是一样的。这意味着只有用户ID为u0_a2或者用户组ID为u0_a2的那些进程才可以对这个calendar.db文件进行读写操作。我们通过执行终端上执行ps命令来查看一下哪个进程的用户ID为u0_a2:
u0_a2 1745 211 983520 60844 SyS_epoll_ a645a7a4 S com.android.providers.calendar
这里我们看到,正是这个日历应用程序com.android.providers.calendar进程的用户ID为u0_a2。这样,就验证了我们前面的分析了:只有这个日历应用程序com.android.providers.calendar才可以对这个calendar.db文件进行读写操作。这个日历应用程序com.android.providers.calendar其实是由一个Content Provider组件来实现的。
Android系统对应用程序的数据文件作如此严格的保护会不会有点过头了呢?如果一个应用程序想要读写另外一个应用程序的数据文件时,应该怎么办呢?举一个典型的应用场景,我们在开发自己的应用程序时,有时候会希望读取通讯录里面某个联系人的手机号码或者电子邮件,以便可以对这个联系人打电话或者发送电子邮件,这时候就需要读取通讯录里面的联系人数据文件了。这时候就需要使用Content Ptovider组件来共享通讯录中的联系人信息了。
现在在互联网里面,都流行平台的概念,各大公司都打着开放平台的口号,来吸引第三方来为自己的平台做应用,例如,国外最流行的Fackbook开放平台和Google+开放平台等,国内的有腾讯的Q+开放平台,还有新浪微博开放平台、360的开放平台等。这些开放平台的核心就是要开放用户数据给第三方来使使用,就像前面我们说的Android系统的通讯录,它需要把自己联系人数据开放出来给其它应用程序使用。但是,这些数据都是各个平台自己的核心数据和核心竞争力,它们需要有保护地进行开放。Android系统中的Content Provider应用程序组件正是结合上面分析的这种文件权限机制来秉承这种有保护地开放自己
的数据给其它应用程序使用的理念。
由于Content组件可以在不同的应用程序之间进行数据共享,因此,他在软件平台建设中是非常有用的。一个软件品台无非就是由数据和业务组成的,一般来说,数据是统一的,而业务则是百花齐放的。由于不同的业务均使用到了统一的数据,因此,从垂直的方向看,一个软件平台至少要由数据层,数据访问层和业务层构成。而从水平方向看,业务层应该由一系列相对独立地方模块组成,以便实现可以适应越来越复杂的业务环境。在Android系统中,数据层可以使用数据库,文件或者网络来实现,业务层可以使用一系列应用来实现,而数据访问层可以使用 Content Ptovider组件实现,整个软件平台架构如下图所示。
在这个架构中, 数据层采用数据库、文件或者网络来保存数据,数据访问层使用Content Provider来实现,而业务层就通过一些APP来实现。为了降低各个功能模块间耦合性,我们可以把业务层的各个APP和数据访问层中的Content Provider,放在不同的应用程序进程中来实现,而数据库中的数据统一由Content Provider来管理,即Content Provider拥有对这些文件直接进行读写的权限,同时,它又根据需要来有保护地把这些数据开放出来给上层的APP来使用。
现在关键的问题是,Content Provider组件是如何将它里面的数据共享给业务层中的Android应用程序访问的呢?我们注意到,Content Provider组件是运行在一个独立的应用程序中的,即它本身也是一个Android应用程序,因此,业务层中的Android应用程序是肯定不可以直接访问它的数据的。从前面的学习可以知道,不同的应用程序进程之间可以使用Binder进程间通信机制来通信。因此,Content Provider组件就可以通过Binder进程间通信机制将它里面的数据传递给业务层中的Android应用程序。然而,事情没有那么简单,因为Content Provider组件一次传递给业务层中的Android应用程序的数据量可能是非常大的。在这种情况下,如果直接使用Binder进程间通信机制来传递数据,那么数据传输效率就会成为问题。我们可以知道,不同的应用程序进程可以通过匿名共享内存来传输大数据,因此无论多大的数据,对于匿名共享内存来说,需要在进程间传递的仅仅是一个文件描述符而已。把需要在进程间传输的数据都写到共享内存中去,然后只能通过Binder进程间通信机制来传输一个共享内存的打开文件描述符给对方就好了,这样,结合Binder进程间通信机制以及匿名共享内存机制,Content Provider组件就可以高效地将它里面的数据传递给业务层中的Android应用程序访问了。
二 SQLite数据库概述
在继续介绍这个应用程序的实现之前,我们先介绍一下这个应用程序用来保存文章信息的数据库的设计。我们知道,在Android系统中,内置了一款轻型的数据库SQLite。SQLite是专门为嵌入式产品而设计的,它具有占用资源低的特点,而且是开源的,非常适合在Android平台中使用,关于SQLite的更多信息可以访问官方网站http://www.sqlite.org。
ArticlesProvider应用程序就是使用SQLite来作为数据库保存文章信息的,数据库文件命名为Articles.db,它里面只有一张表ArticlesTable,表的结构如下所示:
它由四个字段表示,如下
第一个字段: _id字段 --- 表示文章的ID,类型为自动递增的integer,它作为表的key值;
第二个字段:_title字段 --- 表示文章的题目,类型为text;
第三个字段:_abstract字段 --- 表示文章的摘要,类型为text;
第四个字段:_url字段 --- 表示文章的URL,类型为text;
三 ArticlesProvider应用程序的实现
应用程序ArticlesProvider目录结构如下:
-----AndroidManifest.xml
-----Android.mk
-----src
-----com/android/articles
-----Articles.java
-----ArticlesProvider.java
-----res
-----values
-----strings.xml
-----drawable
-----icon.png
Articles.java
package com.android.articles;
import android.net.Uri;
public class Articles {
/*
ID ,TITLE ,ABSTRACT ,URL 四个常量前面已经解释过了,他是我们用来保存文章信息的数据表的四个列名。
*/
public static final String ID = "_id";
public static final String TITLE = "_title";
public static final String ABSTRACT = "_abstract";
public static final String URL = "_url";
/*
DEFAULT_SORT_ORDER 常量是调用ContentProvider接口的query函数来查询数据时调用的,他表示对查询结果按照 _id列的值
从小到大排列;
*/
public static final String DEFAULT_SORT_ORDER = "_id asc";
/*
METHOD_GET_ITEM_COUNT 常量和 KEY_ITEM_COUNT 常量两个常量是调用ContentProvider接口的一个未公开函数call来查询数
据时用的,使用call函数时,传入参数 METHOD_GET_ITEM_COUNT 表示我们要调用我们自己定义的 ContentProvider子
类中的getItemCount函数来获取数据库中的文章信息条目的数量,结果放在一个Bundle中以KEY_ITEM_COUNT为关键字的域中。
*/
public static final String METHOD_GET_ITEM_COUNT = "METHOD_GET_ITEM_COUNT";
public static final String KEY_ITEM_COUNT = "KEY_ITEM_COUNT";
/* URI B组件 */
public static final String AUTHORITY = "com.android.articles";
/*
ITEM、ITEM_ID和POS_ID三个常量分别定了三个URI匹配规则的匹配码
如果URI的形式为content://com.android.providers.articles/item,则匹配规则返回的匹配码为ITEM
如果URI的形式为content://com.android.providers.articles/item/#,#表示一个数字,匹配规则返回的匹配码为ITEM_ID
如果URI的形式为#也是表示任意一个数字,则匹配规则返回的匹配码为ITEM_POS
*/
public static final int ITEM = 1;
public static final int ITEM_ID = 2;
public static final int ITEM_POS = 3;
/*
每一个URI所访问的资源都是一种特定的数据类型,这些数据类型使用多功能Internet邮件扩展协议(MIME)来描述,
MIME定义形式为 "[type]/[subtype]",其中,[type]用来描述数据类型的大类,而[subtype]用来描述数据类型的子类
Content Provider组件规定,他所返回的资源的数据类型统一划分为如下两个大类:
"vnd.android.cursor.dir" 描述数据集合
"vnd.android.cursor.item" ,描述一个个体数据
Content Provider组件所返回的资源的数据类型的子类则是可以自定义的,它的定义形式一般约定为
"vnd.[company name].[resource type]"。
此处两种MIME类型,大类不同,子类一致
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.android.article";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.android.article";
/* 定义两个URI 通用资源标志符 通过它用来唯一标志某个资源位置
CONTENT_URI 表示的URI表示是通过ID来访问文章信息的
CONTENT_POS_URI 表示的URI表示是通过位置来访问文章信息的
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
public static final Uri CONTENT_POS_URI = Uri.parse("content://" + AUTHORITY + "/pos");
}
URI的全称是Universal Resource Identifier,即通用资源标志符,通过它用来唯一标志某个资源在网络中的位置,它的结构和我们常见的HTTP形式URL是一样的,其实我们可以把常见的HTTP形式的URL看成是URI结构的一个实例,URI是在更高一个层次上的抽象。
在Android系统中,它也定义了自己的用来定义某个特定的Content Provider的URI结构,它通常由四个组件来组成,如下所示:
A组件称为Scheme它固定为content://,表示它后面的路径所表示的资源是由Content Provider来提供的。
B组件称为Authority,它唯一地标识了一个特定的Content Provider,因此,这部分内容一般使用Content Provider所在的package来命名,使得它是唯一的。
C组件称为资源路径,它表示所请求的资源的类型,这部分内容是可选的。如果我们自己所实现的Content Provider只提供一种类型的资源访问,那么这部分内部就可以忽略;如果我们自己实现的Content Provider同时提供了多种类型的资源访问,那么这部分内容就不可以忽略了。例如,我们有两种电脑资源可以提供给用户访问,一种是笔记本电脑,一种是平板电脑,我们就把分别它们定义为notebook和pad;如果我们想进一步按照系统类型来进一步细分这两种电脑资源,对笔记本电脑来说,一种是安装了windows系统的,一种是安装了linux系统的,我们就分别把它们定义为notebook/windows和notebook/linux;对平板电脑来说,一种是安装了ios系统的,一种是安装了android系统的,我们就分别把它们定义为pad/ios和pad/android。
D组件称为资源ID,它表示所请求的是一个特定的资源,它通常是一个数字,对应前面我们所介绍的数据库表中的_id字段的内容,它唯一地标志了某一种资源下的一个特定的实例。继续以前面的电脑资源为例,如果我们请求的是编号为123的装了android系统的平板电脑,我们就把它定义为pad/android/123。当忽略这部分内容时,它有可能是表示请求某一种资源下的所有实例,取决于我们的URI匹配规则,后面我们将会进一步解释如何设置URI匹配规则。
回到上面的Articles.java源文件中,我们定义了两个URI,分别用COTENT_URI和CONTENT_POS_URI两个常量来表示,它们的Authority组件均指定为com.android.articles。其中,COTENT_URI常量表示的URI表示是通过ID来访问文章信息的,而CONTENT_POS_URI常量表示的URI表示是通过位置来访问文章信息的。
例如,content://com.android.articles/item表示访问所有的文章信息条目;
content://com.android.articles/item/123表示只访问ID值为123的文章信息条目;
content://com.android.articles/pos/1表示访问数据库表中的第1条文章信息条目,这条文章信息条目的ID值不一定为1。通过常量CONTENT_POS_URI来访问文章信息条目时,必须要指定位置,这也是我们设置的URI匹配规则来指定的,后面我们将会看到。
每一个URI所访问的资源都是一种特定的数据类型,这些数据类型使用多功能Internet邮件扩展协议(MIME)来描述。MIME定义形式为 “[type]/[subtype]”,其中,[type]用来描述数据类型的大类,而[subtype]用来描述数据类型的子类。例如, "text/html"描述的数据类型的大类是文本(text),而子类是超文本标记语言(html)。
Content Provider组件规定,他所返回的资源的数据类型统一划分为"vnd.android.cursor.dir"和 “vnd.android.cursor.item” 两个大类,其中,前者用来描述 一个数据集合,后者用来描述一个个体数据。Content Provider组件所返回的资源的数据类型的子类则是可以自定义的,它的定义形式一般约定为
“vnd.[company name].[resource type]”。其中[company name] 表示一个公司的名称,[resource type]表示一个资源类型,在我们的例子中,CONTENT_TYPE和COTENT_ITEM_TYPE两个常量分别定义了两种MIME类型,它们的大类别分别为vnd.android.cursor.dir和vnd.android.cursor.item,而具体类别均为vdn.com.android.article,其中com.android就是表示公司名了,而article表示资源的类型为文章。这两个MIME类型常量主要是在实现ContentProvider的getType函数时用到的,后面我们将会看到。
ArticlesProvider.java
import java.util.HashMap;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
public class ArticlesProvider extends ContentProvider {
private static final String LOG_TAG = "com.android.providers.articles.ArticlesProvider";
/*
DB_NAME : SQLite数据库名称
DB_VERSION : SQLite数据库版本号
DB_TABLE : 描述这个SQLite数据库中的一个数据库表,ArticlesProvider组件内部的文章信息就保存在里面
*/
private static final String DB_NAME = "Articles.db";
private static final String DB_TABLE = "ArticlesTable";
private static final int DB_VERSION = 1;
/*
定义一条SQL语句 DB_CREATE ,他是用来创建数据库表 DB_TABLE 的
*/
private static final String DB_CREATE = "create table " + DB_TABLE +
" (" + Articles.ID + " integer primary key autoincrement, " +
Articles.TITLE + " text not null, " +
Articles.ABSTRACT + " text not null, " +
Articles.URL + " text not null);";
/*
创建和初始化一个 URI 匹配器 uriMatcher,用来识别一个 URI 类别。匹配器uriMatcher 中有三个匹配规则:
uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM) 对应 URI匹配码 Articles.ITEM
uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID) 对应 URI匹配码 Articles.ITEM_ID
uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS) 对应 URI匹配码 Articles.ITEM_POS
*/
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(Articles.AUTHORITY, "item", Articles.ITEM);
uriMatcher.addURI(Articles.AUTHORITY, "item/#", Articles.ITEM_ID);
uriMatcher.addURI(Articles.AUTHORITY, "pos/#", Articles.ITEM_POS);
}
/*
创建和初始化了一个字段映射表 articleProjectionMap,用来为数据库表ArticlesTable的字段名称设置别名,为数据库表
ArticlesTable的字段名称设置别名之后,ArticlesProvider组件的访问者就不必知道它的真实字段名称了。此处我们将数据
库表ArticlesTable的字段别名和真实名称设置为一样的。在实际应用中,可以根据需要来设置不一样的名称。
*/
private static final HashMap<String, String> articleProjectionMap;
static {
articleProjectionMap = new HashMap<String, String>();
articleProjectionMap.put(Articles.ID, Articles.ID);
articleProjectionMap.put(Articles.TITLE, Articles.TITLE);
articleProjectionMap.put(Articles.ABSTRACT, Articles.ABSTRACT);
articleProjectionMap.put(Articles.URL, Articles.URL);
}
private DBHelper dbHelper = null;
private ContentResolver resolver = null;
/*
ArticlesProvider组件的成员函数 onCreate()在ArticlesProvider组件启动时候调用,主要用来创建一个DBHelper对象和一个ContentResolver
对象,并且分别保存在成员变量dbHelper 和 resolver 中,dbHelper 用来辅助打开一个数据库,resolver 用来通知其他应用程
序,ArticlesProvider组件中的数据发生了更新。
*/
@Override
public boolean onCreate() {
Context context = getContext();
resolver = context.getContentResolver();
dbHelper = new DBHelper(context, DB_NAME, null, DB_VERSION);
Log.i(LOG_TAG, "Articles Provider Create");
return true;
}
/*
ArticlesProvider组件的成员函数getType()用来获取一个URI所要访问的资源的数据类型。只要三类URI可以访
问ArticlesProvider组件,它们分别是:
1
"content://com.android.articles/item"
访问的资源的数据类型为:CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.android.article";
2
"content://com.android.articles/item/#"
访问的资源的数据类型为: CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.android.article";
3
"content://com.android.articles/pos/#"
访问的资源的数据类型为: CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.android.article";
*/
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case Articles.ITEM:
return Articles.CONTENT_TYPE;
case Articles.ITEM_ID:
case Articles.ITEM_POS:
return Articles.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
}
/*
ArticlesProvider组件的成员函数 insert() 用来向ArticlesProvider组件插入一个新的文章条目,参数 values 用来描述一个要
插入的文章条目的信息。并且通过 DBHelper 对象dbHelper打开一个SQL数据库。db.insert()函数将参数 values 所描述的
文章条目写入到这个SQL数据库表中。最后用 ContentResolver 对象方法 resolver.notifyChange() 通知其他应用程
序,ArticlesProvider组件的文章条目已经发生变化。
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
if(uriMatcher.match(uri) != Articles.ITEM) {
throw new IllegalArgumentException("Error Uri: " + uri);
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(DB_TABLE, Articles.ID, values);
if(id < 0) {
throw new SQLiteException("Unable to insert " + values + " for " + uri);
}
Uri newUri = ContentUris.withAppendedId(uri, id);
resolver.notifyChange(newUri, null);
return newUri;
}
/*
ArticlesProvider组件的成员函数 update()用来更新ArticlesProvider组件中的文章条目,
参数selection和selectionArgs用来指定要更新的文章条目,参数values用来描述更新后的文章条目。
首先通过DBHelper 对象dbHelper打开一个SQL数据库,然后switch判断要更新的是一个文章条目,还是一组文章条目,
*/
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count = 0;
switch(uriMatcher.match(uri)) {
case Articles.ITEM: {
count = db.update(DB_TABLE, values, selection, selectionArgs);
break;
}
case Articles.ITEM_ID: {
//从参数 uri 中获取它的文章条目的ID值
String id = uri.getPathSegments().get(1);
count = db.update(DB_TABLE, values, Articles.ID + "=" + id
+ (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
//通知其他应用程序 ArticlesProvider组件的文章条目已经发生变化
resolver.notifyChange(uri, null);
return count;
}
/*
ArticlesProvider组件的成员函数 delete()用来删除组件中的文章条目,参数selection和selectionArgs用来指定要删除的文
章条目。
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
/*
首先通过DBHelper 对象dbHelper打开一个SQL数据库
*/
SQLiteDatabase db = dbHelper.getWritableDatabase();
int count = 0;
/*
判断要删除的是一组文章条目 还是 一个文章条目
*/
switch(uriMatcher.match(uri)) {
case Articles.ITEM: {
count = db.delete(DB_TABLE, selection, selectionArgs);
break;
}
case Articles.ITEM_ID: {
//从参数 uri 中获取它的文章条目的ID值
String id = uri.getPathSegments().get(1);
count = db.delete(DB_TABLE, Articles.ID + "=" + id
+ (!TextUtils.isEmpty(selection) ? " and (" + selection + ')' : ""), selectionArgs);
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
//通知其他应用程序 ArticlesProvider组件的文章条目已经发生变化
resolver.notifyChange(uri, null);
return count;
}
/*
ArticlesProvider组件的成员函数 query() 用来获取 ArticlesProvider组件中的文章条目,参数selection和selectionArgs用来
指定要获取的文章条目,参数projection用来指定要获取的文章条目的字段的的别名。ArticlesProvider组件将最终获得的文
章条目保存你一个Cursor 对象中。
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Log.i(LOG_TAG, "ArticlesProvider.query: " + uri);
/*
首先通过DBHelper 对象dbHelper打开一个SQL数据库
*/
SQLiteDatabase db = dbHelper.getReadableDatabase();
/*
创建一个SQLiteQueryBuilder对象sqlBuilder ,用来准备一条SQL查询语句,以便可以从前面打开的SQL数据库中获得调用者指定的文章条目。
*/
SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
String limit = null;
/*
判断要删除的是一组文章条目 还是 一个文章条目,如果获取的是一个文章条目,那么还需要进一步判断是按照ID值还是位置值来获得这个文章条目
*/
switch (uriMatcher.match(uri)) {
case Articles.ITEM: {
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
break;
}
case Articles.ITEM_ID: {
//如果按照ID值来获取文章条目,从参数 uri 中获取它的文章条目的ID值
String id = uri.getPathSegments().get(1);
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
sqlBuilder.appendWhere(Articles.ID + "=" + id);
break;
}
/*
如果是按照位置值获得文章条目,就从参数 uri 中获取它的文章条目的位置值
*/
case Articles.ITEM_POS: {
String pos = uri.getPathSegments().get(1);
sqlBuilder.setTables(DB_TABLE);
sqlBuilder.setProjectionMap(articleProjectionMap);
limit = pos + ", 1";
break;
}
default:
throw new IllegalArgumentException("Error Uri: " + uri);
}
/*
通过 SQLiteQueryBuilder 对象sqlBuilder 来从前面打开的SQLite数据库中获得指定的文章条目。并且保存在一个
cursor 对象中。在将这个cursor 对象返回给调用者之前,将 resolver对象设置到他里面去,以便保存在它里面的文章
条目发生变化时,可以通过这个resolver对象通知其他应用程序,ArticlesProvider组件的文章条目发生变化了。
*/
Cursor cursor = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, TextUtils.isEmpty(sortOrder) ? Articles.DEFAULT_SORT_ORDER : sortOrder, limit);
cursor.setNotificationUri(resolver, uri);
return cursor;
}
/*
成员函数 call()是一个未公开的数据查询接口。如果一个应用程序想通过这个未公开的接口来获取一个 Content Provider组件的数据,那么它必
须在Android源代码环境中进行编译。
函数中只提供一个数据获取操作 Articles.METHOD_GET_ITEM_COUNT,用来获取ArticlesProvider组件中的文章条目总数。
*/
@Override
public Bundle call(String method, String request, Bundle args) {
Log.i(LOG_TAG, "ArticlesProvider.call: " + method);
if(method.equals(Articles.METHOD_GET_ITEM_COUNT)) {
return getItemCount();
}
throw new IllegalArgumentException("Error method call: " + method);
}
private Bundle getItemCount() {
Log.i(LOG_TAG, "ArticlesProvider.getItemCount");
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select count(*) from " + DB_TABLE, null);
int count = 0;
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
}
Bundle bundle = new Bundle();
bundle.putInt(Articles.KEY_ITEM_COUNT, count);
cursor.close();
db.close();
return bundle;
}
/*
实现了一个用来打开SQLite数据库的辅助类DBHelper
*/
private static class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context, String name, CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
onCreate(db);
}
}
}
我们在实现自己的Content Provider时,必须继承于ContentProvider类,并且实现以下六个函数:
-- onCreate(),用来执行一些初始化的工作。
-- query(Uri, String[], String, String[], String),用来返回数据给调用者。
-- insert(Uri, ContentValues),用来插入新的数据。
-- update(Uri, ContentValues, String, String[]),用来更新已有的数据。
-- delete(Uri, String, String[]),用来删除数据。
-- getType(Uri),用来返回数据的MIME类型。
成员函数 call()是一个未公开的数据查询接口。如果一个应用程序想通过这个未公开的接口来获取一个 Content Provider组件的数据,那么它必须在Android源代码环境中进行编译。一般来说一个应用程序想要获取Content Provider组件中的数据,那么只能调用它的标准数据查询接口,即调用成员函数query()就可以了。既然如此,为什么还要提供非标准的数据查询接口呢? Content Provider组件的成员函数 query()和call()最大的区别在于,query()将要获取的数据保存在一个Cursor对象中传输给调用者,call()将要获取的数据保存在一个Bundle对象中传输给调用者。保存在Cursor对象中的数据是通过匿名共享内存来传输的,而保存在Bundle对象中的数据是直接使用Binder进程间通信机制来传输的。当传输数据量很大时,使用匿名共享内存是有好处的,但是当传输数据量比较小时,使用匿名共享内存就得不偿失了,因为匿名共享内存不是免费的午餐,它的创建过程和映射过程都是需要开销的。因此 Content Provider组件就提供了这个未公开的成员函数接口call()来方便调用者可以快速的获取一些量比较小的数据。
辅助类DBHelper继承自 SQLiteOpenHelper类,由于打开数据库涉及到IO操作,因此,我们应该避免在
ArticlesProvider组件启动时区打开它内部的数据库,即避免在它的成员函数onCreate中执行打开数据的操作。否则,ArticlesProvider组件可能会由于启动超时而被系统中止,那么ArticlesProvider组件应该在什么时候执行打开数据库操作呢?最好的时机莫过于第一次使用这个数据可的时候了。
SQLiteOpenHelper类就是这样一个类,当他的成员函数 getReadableDatabase或者getWritableDatabase第一次被调用时,他才会打开与它关联的一个SQLite数据库。我们一般不会直接使用SQLiteOpenHelper类来辅助打开一个SQLite数据库,而是以它为父类实现一个子类,并且在子类中重写它的成员函数onCreate() 和 onUpgrade() 。
四 Article应用程序
应用程序Article由两个Activity组件 MainActivity和ArticleActivity组成,他们用来浏览、添加、修改、删除ArticlesProvider组件中的文章信息。
-----AndroidManifest.xml
-----Android.mk
-----src
-----com/android/articles
-----Article.java
-----MainActivity.java
-----ArticleActivity.java
-----ArticlesAdapter.java
-----libs
-----ArticlesProvider.jar
-----res
-----layout
-----main.xml
-----article.xml
-----item.xml
-----values
-----strings.xml
-----drawable
-----icon.png
-----border.xml
Article.java
package com.android.article;
public class Article {
private int id;
private String title;
private String abs;
private String url;
public Article(int id, String title, String abs, String url) {
this.id = id;
this.title = title;
this.abs = abs;
this.url = url;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return this.id;
}
public void setTitle(String title) {
this.title = title;
}
public String getTitle() {
return this.title;
}
public void setAbstract(String abs) {
this.abs = abs;
}
public String getAbstract() {
return this.abs;
}
public void setUrl(String url) {
this.url = url;
}
public String getUrl() {
return this.url;
}
}
Article.java定义了一个Article类,用来封装一个文章条目信息,它里面包含四个变量,id、title、abs、url 分别用来描述文章条目的ID、标题、摘要和URL。
ArticlesAdapter.java
package shy.luo.article;
import java.util.LinkedList;
import shy.luo.providers.articles.Articles;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.IContentProvider;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
public class ArticlesAdapter {
private static final String LOG_TAG = "shy.luo.article.ArticlesAdapter";
private ContentResolver resolver = null;
/*
构造函数中调用参数 context的成员函数getContentResolver()来获得一个ContentResolver对象,并且保存在resolver 中
*/
public ArticlesAdapter(Context context) {
resolver = context.getContentResolver();
}
/*
ArticlesAdapter 类的成员函数insertArticle()用来向 ArticlesProvider组件中插入一个新的文章条目
参数 article 描述要插入的文章条目
*/
public long insertArticle(Article article) {
/* 将参数article内容写入到一个 ContentValues 对象 values 中。*/
ContentValues values = new ContentValues();
values.put(Articles.TITLE, article.getTitle());
values.put(Articles.ABSTRACT, article.getAbstract());
values.put(Articles.URL, article.getUrl());
/*
通过ContentResolver 对象 resolver 将 ContentValues 对象 values 发送给ArticlesProvider组件,以便
ArticlesProvider组件可以向内部数据库插入一个新的文章条目。
*/
Uri uri = resolver.insert(Articles.CONTENT_URI, values);
/* 获得插入成功后的文章条目ID*/
String itemId = uri.getPathSegments().get(1);
return Integer.valueOf(itemId).longValue();
}
/*
用来更新 ArticlesProvider组件中的文章条目
参数article 描述要更新的文章条目
*/
public boolean updateArticle(Article article) {
/*
首先构造一个URI,用来指定要更新的文章条目的ID值
ContentUris是content URI的一个辅助类。它有两个方法很有用,具体如下所示:
public static Uri withAppendedId(Uri contentUri, long id)
这个方法负责把id和contentUri连接成一个新的Uri,如此时ID=100,则连接后为:
content://com.android.articles/item/100
public static long parseId(Uri contentUri)
这个方法负责把content URI 后边的id解析出来
*/
Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, article.getId());
/* 将参数article内容写入到一个 ContentValues 对象 values 中。*/
ContentValues values = new ContentValues();
values.put(Articles.TITLE, article.getTitle());
values.put(Articles.ABSTRACT, article.getAbstract());
values.put(Articles.URL, article.getUrl());
/*
通过ContentResolver 对象 resolver 将 ContentValues 对象 values 发送给ArticlesProvider组件,以便
ArticlesProvider组件可以向内部数据库更新指定的文章条目。
*/
int count = resolver.update(uri, values, null, null);
return count > 0;
}
/*用来删除 ArticlesProvider组件中的文章条目*/
public boolean removeArticle(int id) {
/*首先构造一个URI,用来指定要更新的文章条目的ID值*/
Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id);
/*
通过ContentResolver 对象 resolver 将 ContentValues 对象 values 发送给ArticlesProvider组件,以便
ArticlesProvider组件可以向内部数据库删除指定的文章条目。
*/
int count = resolver.delete(uri, null, null);
return count > 0;
}
/* 用来获取ArticlesProvider组件中所有的文章条目 */
public LinkedList<Article> getAllArticles() {
LinkedList<Article> articles = new LinkedList<Article>();
String[] projection = new String[] {
Articles.ID,
Articles.TITLE,
Articles.ABSTRACT,
Articles.URL
};
/* ArticlesProvider组件返回的文章条目保存在一个Cursor 对象中 */
Cursor cursor = resolver.query(Articles.CONTENT_URI, projection, null, null, Articles.DEFAULT_SORT_ORDER);
/* 遍历cursor 对象内容,将里面的每一个文章条目都封装成 article 对象,保存在LinkedList列表中*/
if (cursor.moveToFirst()) {
do {
int id = cursor.getInt(0);
String title = cursor.getString(1);
String abs = cursor.getString(2);
String url = cursor.getString(3);
Article article = new Article(id, title, abs, url);
articles.add(article);
} while(cursor.moveToNext());
}
/* 将LinkedList列表返还给调用者 */
return articles;
}
/* 用来获取ArticlesProvider组件中所有的文章条目总数,是通过调用ArticlesProvider组件的成员函数call()实现的,总数保存
在Bundle对象中 */
public int getArticleCount() {
int count = 0;
try {
IContentProvider provider = resolver.acquireProvider(Articles.CONTENT_URI);
Bundle bundle = provider.call(Articles.METHOD_GET_ITEM_COUNT, null, null);
count = bundle.getInt(Articles.KEY_ITEM_COUNT, 0);
} catch(RemoteException e) {
e.printStackTrace();
}
return count;
}
/* 根据文章的ID值从ArticlesProvider组件中获取指定的文章条目,即通过URI=CONTENT_URI */
public Article getArticleById(int id) {
Uri uri = ContentUris.withAppendedId(Articles.CONTENT_URI, id);
String[] projection = new String[] {
Articles.ID,
Articles.TITLE,
Articles.ABSTRACT,
Articles.URL
};
Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER);
Log.i(LOG_TAG, "cursor.moveToFirst");
if (!cursor.moveToFirst()) {
return null;
}
String title = cursor.getString(1);
String abs = cursor.getString(2);
String url = cursor.getString(3);
return new Article(id, title, abs, url);
}
/* 根据文章的位置值从ArticlesProvider组件中获取指定的文章条目,即通过URI=CONTENT_POS_URI */
public Article getArticleByPos(int pos) {
Uri uri = ContentUris.withAppendedId(Articles.CONTENT_POS_URI, pos);
String[] projection = new String[] {
Articles.ID,
Articles.TITLE,
Articles.ABSTRACT,
Articles.URL
};
Cursor cursor = resolver.query(uri, projection, null, null, Articles.DEFAULT_SORT_ORDER);
if (!cursor.moveToFirst()) {
return null;
}
int id = cursor.getInt(0);
String title = cursor.getString(1);
String abs = cursor.getString(2);
String url = cursor.getString(3);
return new Article(id, title, abs, url);
}
}
MainActivity.java
package com.android.article;
import shy.luo.providers.articles.Articles;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
public class MainActivity extends Activity implements View.OnClickListener, AdapterView.OnItemClickListener {
private final static String LOG_TAG = "shy.luo.article.MainActivity";
private final static int ADD_ARTICAL_ACTIVITY = 1;
private final static int EDIT_ARTICAL_ACTIVITY = 2;
private ArticlesAdapter aa = null;
private ArticleAdapter adapter = null;
private ArticleObserver observer = null;
private ListView articleList = null;
private Button addButton = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
/* 创建ArticlesAdapter对象 aa 用来辅助访问ArticlesProvider组件中的文章信息 */
aa = new ArticlesAdapter(this);
articleList = (ListView)findViewById(R.id.listview_article);
/* 创建ArticlesAdapter对象adapter 用来作为界面上一个文章列表控件的数据源 */
adapter = new ArticleAdapter(this);
articleList.setAdapter(adapter);
articleList.setOnItemClickListener(this);
/* 创建和注册 ArticleObserver对象observer 用来监控ArticlesProvider组件中的文章信息变化*/
observer = new ArticleObserver(new Handler());
getContentResolver().registerContentObserver(Articles.CONTENT_URI, true, observer);
addButton = (Button)findViewById(R.id.button_add);
addButton.setOnClickListener(this);
Log.i(LOG_TAG, "MainActivity Created");
}
/* 用于注销前面所注册的 ArticleObserver对象observer ,表示不需要监控ArticlesProvider组件中的文章信息变化了*/
@Override
public void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(observer);
}
@Override
public void onClick(View v) {
if(v.equals(addButton)) {
Intent intent = new Intent(this, ArticleActivity.class);
startActivityForResult(intent, ADD_ARTICAL_ACTIVITY);
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
Intent intent = new Intent(this, ArticleActivity.class);
Article article = aa.getArticleByPos(pos);
intent.putExtra(Articles.ID, article.getId());
intent.putExtra(Articles.TITLE, article.getTitle());
intent.putExtra(Articles.ABSTRACT, article.getAbstract());
intent.putExtra(Articles.URL, article.getUrl());
startActivityForResult(intent, EDIT_ARTICAL_ACTIVITY);
}
@Override
public void onActivityResult(int requestCode,int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch(requestCode) {
case ADD_ARTICAL_ACTIVITY: {
if(resultCode == Activity.RESULT_OK) {
String title = data.getStringExtra(Articles.TITLE);
String abs = data.getStringExtra(Articles.ABSTRACT);
String url = data.getStringExtra(Articles.URL);
Article article = new Article(-1, title, abs, url);
aa.insertArticle(article);
}
break;
}
case EDIT_ARTICAL_ACTIVITY: {
if(resultCode == Activity.RESULT_OK) {
int action = data.getIntExtra(ArticleActivity.EDIT_ARTICLE_ACTION, -1);
if(action == ArticleActivity.MODIFY_ARTICLE) {
int id = data.getIntExtra(Articles.ID, -1);
String title = data.getStringExtra(Articles.TITLE);
String abs = data.getStringExtra(Articles.ABSTRACT);
String url = data.getStringExtra(Articles.URL);
Article article = new Article(id, title, abs, url);
aa.updateArticle(article);
} else if(action == ArticleActivity.DELETE_ARTICLE) {
int id = data.getIntExtra(Articles.ID, -1);
aa.removeArticle(id);
}
}
break;
}
}
}
private class ArticleObserver extends ContentObserver {
public ArticleObserver(Handler handler) {
super(handler);
}
@Override
public void onChange (boolean selfChange) {
adapter.notifyDataSetChanged();
}
}
private class ArticleAdapter extends BaseAdapter {
private LayoutInflater inflater;
public ArticleAdapter(Context context){
inflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return aa.getArticleCount();
}
@Override
public Object getItem(int pos) {
return aa.getArticleByPos(pos);
}
@Override
public long getItemId(int pos) {
return aa.getArticleByPos(pos).getId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Article article = (Article)getItem(position);
if (convertView == null) {
convertView = inflater.inflate(R.layout.item, null);
}
TextView titleView = (TextView)convertView.findViewById(R.id.textview_article_title);
titleView.setText("Title: " + article.getTitle());
TextView abstractView = (TextView)convertView.findViewById(R.id.textview_article_abstract);
abstractView.setText("Abstract: " + article.getAbstract());
TextView urlView = (TextView)convertView.findViewById(R.id.textview_article_url);
urlView.setText("URL: " + article.getUrl());
return convertView;
}
}
}
在应用程序的主界面中,我们使用一个ListView来显示文章信息条目,这个ListView的数据源由ArticleAdapter类来提供,而ArticleAdapter类又是通过ArticlesAdapter类来获得ArticlesProvider中的文章信息的。在MainActivity的onCreate函数,我们还通过应用程序上下文的ContentResolver接口来注册了一个ArticleObserver对象来监控ArticlesProvider中的文章信息。一旦ArticlesProvider中的文章信息发生变化,就会通过ArticleAdapter类来实时更新ListView中的文章信息。
ArticleActivity.java
package shy.luo.article;
import shy.luo.providers.articles.Articles;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class ArticleActivity extends Activity implements View.OnClickListener {
private final static String LOG_TAG = "shy.luo.article.ArticleActivity";
public final static String EDIT_ARTICLE_ACTION = "EDIT_ARTICLE_ACTION";
public final static int MODIFY_ARTICLE = 1;
public final static int DELETE_ARTICLE = 2;
private int articleId = -1;
private EditText titleEdit = null;
private EditText abstractEdit = null;
private EditText urlEdit = null;
private Button addButton = null;
private Button modifyButton = null;
private Button deleteButton = null;
private Button cancelButton = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.article);
titleEdit = (EditText)findViewById(R.id.edit_article_title);
abstractEdit = (EditText)findViewById(R.id.edit_article_abstract);
urlEdit = (EditText)findViewById(R.id.edit_article_url);
addButton = (Button)findViewById(R.id.button_add_article);
addButton.setOnClickListener(this);
modifyButton = (Button)findViewById(R.id.button_modify);
modifyButton.setOnClickListener(this);
deleteButton = (Button)findViewById(R.id.button_delete);
deleteButton.setOnClickListener(this);
cancelButton = (Button)findViewById(R.id.button_cancel);
cancelButton.setOnClickListener(this);
Intent intent = getIntent();
articleId = intent.getIntExtra(Articles.ID, -1);
if(articleId != -1) {
String title = intent.getStringExtra(Articles.TITLE);
titleEdit.setText(title);
String abs = intent.getStringExtra(Articles.ABSTRACT);
abstractEdit.setText(abs);
String url = intent.getStringExtra(Articles.URL);
urlEdit.setText(url);
addButton.setVisibility(View.GONE);
} else {
modifyButton.setVisibility(View.GONE);
deleteButton.setVisibility(View.GONE);
}
Log.i(LOG_TAG, "ArticleActivity Created");
}
@Override
public void onClick(View v) {
if(v.equals(addButton)) {
String title = titleEdit.getText().toString();
String abs = abstractEdit.getText().toString();
String url = urlEdit.getText().toString();
Intent result = new Intent();
result.putExtra(Articles.TITLE, title);
result.putExtra(Articles.ABSTRACT, abs);
result.putExtra(Articles.URL, url);
setResult(Activity.RESULT_OK, result);
finish();
} else if(v.equals(modifyButton)){
String title = titleEdit.getText().toString();
String abs = abstractEdit.getText().toString();
String url = urlEdit.getText().toString();
Intent result = new Intent();
result.putExtra(Articles.ID, articleId);
result.putExtra(Articles.TITLE, title);
result.putExtra(Articles.ABSTRACT, abs);
result.putExtra(Articles.URL, url);
result.putExtra(EDIT_ARTICLE_ACTION, MODIFY_ARTICLE);
setResult(Activity.RESULT_OK, result);
finish();
} else if(v.equals(deleteButton)) {
Intent result = new Intent();
result.putExtra(Articles.ID, articleId);
result.putExtra(EDIT_ARTICLE_ACTION, DELETE_ARTICLE);
setResult(Activity.RESULT_OK, result);
finish();
} else if(v.equals(cancelButton)) {
setResult(Activity.RESULT_CANCELED, null);
finish();
}
}
}
在ArticleActivity窗口中,我们可以执行新增、更新和删除文章信息的操作。如果启动ArticleActivity时,没有把文章ID传进来,就说明要执行操作是新增文章信息;如果启动ArticleActivity时,把文章ID和其它信自都传进来了,就说明要执行的操作是更新或者删除文章,根据用户在界面点击的是更新按钮还是删除按钮来确定。