1、ContentProvider、ContentResolver和ContentObserver
ContentProvider是Android的四大组件之一,可见它在Android中 的作用非同小可。它主要的作用是:实现各个应用程序之间的(跨应用)数据共享,比如联系人应用中就使用了ContentProvider,你在自己的应用 中可以读取和修改联系人的数据,不过需要获得相应的权限。其实它也只是一个中间人,真正的数据源是文件或者SQLite等。
一个应用实现ContentProvider来提供内容给别的应用来操作, 通过ContentResolver来操作别的应用数据,当然在自己的应用中也可以。
ContentObserver——内容观察者,目的是观察(捕捉)特定Uri引起的数据库的变化,继而做一些相应的处理,它类似于数据库技术中的
触发器(Trigger),当ContentObserver所观察的Uri发生变化时,便会触发它。触发器分为表触发器、行触发器,相应地
ContentObserver也分为“表“ContentObserver、“行”ContentObserver,当然这是与它所监听的Uri
MIME Type有关的。
2、Contacts Demo
1)、基本功能实现
接下来通过一个简单的存储联系人信息的demo,来学习怎么创建自定义的ContentProvider,这里数据源选用SQLite,最常用的也是这个。
(1) 创建一个类NoteContentProvider,继承ContentProvider,需要实现下面5个方法:
query
insert
update
delete
getType
01.
public
class
ContactsContentProvider
extends
ContentProvider{
02.
03.
@Override
04.
public
boolean
onCreate() {
05.
// TODO Auto-generated method stub
06.
return
false
;
07.
}
08.
09.
@Override
10.
public
int
delete(Uri arg0, String arg1, String[] arg2) {
11.
// TODO Auto-generated method stub
12.
return
0
;
13.
}
14.
15.
@Override
16.
public
String getType(Uri arg0) {
17.
// TODO Auto-generated method stub
18.
return
null
;
19.
}
20.
21.
@Override
22.
public
Uri insert(Uri arg0, ContentValues arg1) {
23.
// TODO Auto-generated method stub
24.
return
null
;
25.
}
26.
27.
@Override
28.
public
Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3,
29.
String arg4) {
30.
// TODO Auto-generated method stub
31.
return
null
;
32.
}
33.
34.
@Override
35.
public
int
update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
36.
// TODO Auto-generated method stub
37.
return
0
;
38.
}
39.
40.
}
(2)先来设计一个数据库,用来联系人信息,主要包含_ID,name,telephone,create_date,content五个字段。
group_name字段等后面升级部分再做使用。创建ProviderMetaData类,封装URI和数据库、表、字段相关信息,源码如下:
01.
public
class
ProviderMetaData {
02.
03.
public
static
final
String AUTHORITY =
"com.johnny.contactsprovider"
;
05.
06.
public
static
final
class
ContactsData
implements
BaseColumns{
07.
public
static
final
String TABLE_NAME =
"contacts"
;
08.
public
static
final
Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE_NAME);
09.
10.
public
static
final
String CONTENT_TYPE =
"vnd.android.cursor.dir/contact"
;
11.
public
static
final
String CONTENT_ITEM_TYPE =
"vnd.android.cursor.item/contact"
;
12.
13.
public
static
final
String CONTACT_NAME =
"name"
;
14.
public
static
final
String CONTACT_TELEPHONE =
"telephone"
;
15.
public
static
final
String CONTACT_CREATE_DATE =
"create_date"
;
16.
public
static
final
String CONTACT_CONTENT =
"content"
;
17.
public
static
final
String CONTACT_GROUP =
"group_name"
;
18.
19.
public
static
final
String DEFAULT_ORDERBY =
"create_date DESC"
;
20.
21.
public
static
final
String SQL_CREATE_TABLE =
"CREATE TABLE "
+ TABLE_NAME +
" ("
22.
+ _ID +
" INTEGER PRIMARY KEY,"
23.
+ CONTACT_NAME +
" VARCHAR(50),"
24.
+ CONTACT_TELEPHONE +
" VARCHAR(11),"
25.
+ CONTACT_CONTENT +
" TEXT,"
26.
+ CONTACT_CREATE_DATE +
" INTEGER"
27.
+
");"
;
28.
}
29.
}
AUTHORITY代表授权,该字符串和在Android描述文件AndroidManifest.xml中注册该ContentProvider时的
android:authorities值一样,ContactsData继承BaseColumns,后者提供了标准的_id字段,表示行ID。
熟悉Content Provider(内容提供者)的应该知道,我们可以通过UriMatcher类注册不同类型的Uri,我们可以通过这些不同的Uri来查询不同的结果。根据Uri返回的结果,Uri Type可以分为:返回多条数据的Uri、返回单条数据的Uri。
Android遵循类似的约定来定义MIME类型,每个内容类型的Android MIME类型有两种形式:多条记录(集合)和单条记录。
多条记录
vnd.android.cursor.dir/contact
单条记录
vnd.android.cursor.item/contact
vnd表示这些类型和子类型具有非标准的、供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型/之
后的内容可以按照格式随便填写。在使用Intent时,会用到MIME这玩意,根据Mimetype打开符合条件的活动。
(3) ContentProvider是根据URI来获取数据的,那它怎么区分不同的URI呢,因为无论是获取笔记列表还是获取一条笔记都是调用query方法,现在来实现这个功能。需要用到类UriMatcher,该类可以帮助我们识别URI类型,下面看实现源码:
01.
static
final
UriMatcher URI_MATCHER =
new
UriMatcher(UriMatcher.NO_MATCH);
02.
static
final
HashMap<String, String> CONTACTS_PROJECTION_MAP =
new
HashMap<String, String>();
03.
private
static
final
int
CONTACTS =
1
;
04.
private
static
final
int
CONTACTS_ID =
2
;
05.
static
{
06.
final
UriMatcher matcher = URI_MATCHER;
07.
matcher.addURI(ProviderMetaData.AUTHORITY,
"contacts"
, CONTACTS);
08.
matcher.addURI(ProviderMetaData.AUTHORITY,
"contacts/#"
, CONTACTS_ID);
09.
10.
HashMap<String, String> map = CONTACTS_PROJECTION_MAP;
11.
map.put(ContactsData._ID, ContactsData._ID);
12.
map.put(ContactsData.CONTACT_NAME, ContactsData.CONTACT_NAME);
13.
map.put(ContactsData.CONTACT_TELEPHONE, ContactsData.CONTACT_TELEPHONE);
14.
map.put(ContactsData.CONTACT_CONTENT, ContactsData.CONTACT_CONTENT);
15.
map.put(ContactsData.CONTACT_CREATE_DATE, ContactsData.CONTACT_CREATE_DATE);
16.
}
这段代码是NoteContentProvider类中的,UriMatcher的工作原理:首先需要在UriMatcher中注册URI模式,每一个模
式跟一个唯一的编号关联,注册之后,在使用中就可以根据URI得到对应的编号,当模式不匹配时,UriMatcher将返回一个NO_MATCH常量,这
样就可以区分了。
(4)
还需为查询设置一个投影映射,主要是将抽象字段映射到数据库中真实的字段名称,因为这些字段有时是不同的名称,既抽象字段的值可以不跟数据库中的字段名称
一样。这里使用HashMap来完成,key是抽象字段名称,value对应数据库中的字段名称,不过这里我把两者的值设置是一样的,在
NoteContentProvider.java中添加如上面所示的代码。
(5) 在NoteContentProvider.java中创建一个内部类DatabaseHelper,继承自SQLiteOpenHelper,完成数据库表的创建、更新,这样可以通过它获得数据库对象,相关代码如下。
01.
private
class
DatabaseHelper
extends
SQLiteOpenHelper{
02.
03.
static
final
String DATABASE_NAME =
"test.db"
;
04.
static
final
int
DATABASE_VERSION =
1
;
05.
06.
public
DatabaseHelper(Context context) {
07.
super
(context, DATABASE_NAME,
null
, DATABASE_VERSION);
08.
// TODO Auto-generated constructor stub
09.
}
10.
11.
@Override
12.
public
void
onCreate(SQLiteDatabase db) {
13.
// TODO Auto-generated method stub
14.
db.execSQL(ContactsData.SQL_CREATE_TABLE);
15.
}
16.
17.
@Override
18.
public
void
onUpgrade(SQLiteDatabase db,
int
oldVersion,
int
newVersion) {
19.
// TODO Auto-generated method stub
20.
onCreate(db);
21.
}
22.
23.
}
(6) 现在来分别实现第一步中未实现的5个方法,先来实现query方法,这里借助SQLiteQueryBuilder来为查询设置投影映射以及设置相关查询条件,看源码实现:
01.
@Override
02.
public
Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
03.
String sortOrder) {
04.
// TODO Auto-generated method stub
05.
SQLiteQueryBuilder queryBuilder =
new
SQLiteQueryBuilder();
06.
switch
(URI_MATCHER.match(uri)){
07.
case
CONTACTS_ID:
08.
queryBuilder.setTables(ContactsData.TABLE_NAME);
09.
queryBuilder.setProjectionMap(CONTACTS_PROJECTION_MAP);
10.
queryBuilder.appendWhere(ContactsData.TABLE_NAME +
"._id="
+Long.toString(ContentUris.parseId(uri)));
11.
break
;
12.
case
CONTACTS:
13.
queryBuilder.setTables(ContactsData.TABLE_NAME);
14.
queryBuilder.setProjectionMap(CONTACTS_PROJECTION_MAP);
15.
break
;
16.
}
17.
18.
String orderBy;
19.
if
(TextUtils.isEmpty(sortOrder))
20.
{
21.
orderBy = ContactsData.DEFAULT_ORDERBY;
22.
}
else
{
23.
orderBy = sortOrder;
24.
}
25.
SQLiteDatabase db = mDbHelper.getReadableDatabase();
26.
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null
,
null
, orderBy);
27.
28.
return
cursor;
29.
}
返回的是一个Cursor对象,它是一个行集合,包含0和多个记录,类似于JDBC中的ResultSet,可以前后移动游标,得到每行每列中的数据。注意的是,使用它需要调用moveToFirst(),因为游标默认是在第一行之前。
(7)实现insert方法,实现把记录插入到基础数据库中,然后返回新创建的记录的URI。
01.
@Override
02.
public
Uri insert(Uri uri, ContentValues values) {
03.
// TODO Auto-generated method stub
04.
SQLiteDatabase db = mDbHelper.getWritableDatabase();
05.
long
id = db.insertOrThrow(ContactsData.TABLE_NAME,
null
, values);
06.
07.
// 更新数据时,通知其他ContentObserver
08.
getContext().getContentResolver().notifyChange(ContactsData.CONTENT_URI,
null
);
09.
10.
if
(id >
0
){
11.
return
ContentUris.withAppendedId(uri, id);
12.
}
13.
return
null
;
14.
}
(8) 实现update方法,根据传入的列值和where字句来更新记录,返回更新的记录数,看源码:
01.
@Override
02.
public
int
update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
03.
// TODO Auto-generated method stub
04.
SQLiteDatabase db = mDbHelper.getWritableDatabase();
05.
int
modified =
0
;
06.
switch
(URI_MATCHER.match(uri)){
07.
case
CONTACTS_ID:
08.
selection = DatabaseUtils.concatenateWhere(selection,ContactsData.TABLE_NAME +
"._id=?"
);
09.
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
10.
new
String[]{Long.toString(ContentUris.parseId(uri))});
11.
Log.d(
"Test"
,
"selectionArgs 0"
+selectionArgs);
12.
modified = db.update(ContactsData.TABLE_NAME, values, selection, selectionArgs);
13.
break
;
14.
case
CONTACTS:
15.
modified = db.update(ContactsData.TABLE_NAME, values, selection, selectionArgs);
16.
Log.d(
"Test"
,
"selectionArgs 1"
+selectionArgs);
17.
break
;
18.
}
19.
20.
// 更新数据时,通知其他ContentObserver
21.
getContext().getContentResolver().notifyChange(ContactsData.CONTENT_URI,
null
);
22.
23.
return
modified;
24.
}
notifyChange函数是在更新数据时,通知其他监听对象。
(9)实现delete方法,该方法返回删除的记录数。
01.
@Override
02.
public
int
delete(Uri uri, String selection, String[] selectionArgs) {
03.
// TODO Auto-generated method stub
04.
SQLiteDatabase db = mDbHelper.getWritableDatabase();
05.
int
deleted =
0
;
06.
switch
(URI_MATCHER.match(uri)){
07.
case
CONTACTS_ID:
08.
selection = DatabaseUtils.concatenateWhere(selection,ContactsData.TABLE_NAME +
"._id=?"
);
09.
selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
10.
new
String[]{Long.toString(ContentUris.parseId(uri))});
11.
Log.d(
"Test"
,
"selectionArgs 0"
+selectionArgs);
12.
deleted = db.delete(ContactsData.TABLE_NAME, selection, selectionArgs);
13.
break
;
14.
case
CONTACTS:
15.
deleted = db.delete(ContactsData.TABLE_NAME, selection, selectionArgs);
16.
Log.d(
"Test"
,
"selectionArgs 1"
+selectionArgs);
17.
break
;
18.
}
19.
20.
// 更新数据时,通知其他ContentObserver
21.
getContext().getContentResolver().notifyChange(ContactsData.CONTENT_URI,
null
);
22.
23.
return
deleted;
24.
}
(10) 实现getType方法,根据URI返回MIME类型,这里主要用来区分URI是获取集合还是单条记录,这个方法在这里暂时没啥用处,在使用Intent时有用。
01.
@Override
02.
public
String getType(Uri uri) {
03.
// TODO Auto-generated method stub
04.
switch
(URI_MATCHER.match(uri)){
05.
case
CONTACTS:
06.
return
ContactsData.CONTENT_TYPE;
07.
case
CONTACTS_ID:
08.
return
ContactsData.CONTENT_ITEM_TYPE;
09.
// default:
10.
// throw new IllegalArgumentException("Unknow URI: " + uri);
11.
}
12.
return
null
;
13.
}
(11) 在AndroidManifest.xml中注册该ContentProvider,这样系统才找得到,当然你也可以设置相关的权限,这里就不设置了
1.
<provider
2.
android:name=
"com.johnny.testcontentprovider.ContactsContentProvider"
3.
android:authorities=
"com.johnny.contactsprovider"
>
4.
5.
</provider>
(12)到现在为止,自定义ContentProvider的全部代码已经完成,下面创建一个简单的应用来测试一下。
主要测试insert、update、delete、query这四个函数。
01.
private
void
insertContact1(){
02.
ContentValues values =
new
ContentValues();
03.
values.put(ContactsData.CONTACT_NAME,
"James"
);
04.
values.put(ContactsData.CONTACT_TELEPHONE,
"18888888888"
);
05.
values.put(ContactsData.CONTACT_CONTENT,
"NBA Star"
);
06.
values.put(ContactsData.CONTACT_CREATE_DATE, System.currentTimeMillis());
07.
Uri uri = getContentResolver().insert(ContactsData.CONTENT_URI, values);
08.
Log.d(
"Test"
,
"uri = "
+uri);
09.
}
10.
11.
private
void
deleteContact1(){
12.
int
count = getContentResolver().delete(ContactsData.CONTENT_URI, ContactsData.CONTACT_NAME+
"='James'"
,
null
);
13.
Log.d(
"Test"
,
"count = "
+count);
14.
}
15.
16.
private
void
updateContact1(){
17.
ContentValues values =
new
ContentValues();
18.
values.put(ContactsData.CONTACT_TELEPHONE,
"16666666666"
);
19.
int
count = getContentResolver().update(ContactsData.CONTENT_URI,values, ContactsData.CONTACT_NAME+
"='James'"
,
null
);
20.
Log.d(
"Test"
,
"count = "
+count);
21.
}
22.
23.
private
void
queryContact1(){
24.
Cursor cursor =
this
.getContentResolver().query(ContactsData.CONTENT_URI,
null
, ContactsData.CONTACT_NAME+
"='James'"
,
null
,
null
);
25.
Log.e(
"test "
,
"count="
+ cursor.getCount());
26.
cursor.moveToFirst();
27.
while
(!cursor.isAfterLast()) {
28.
String name = cursor.getString(cursor.getColumnIndex(ContactsData.CONTACT_NAME));
29.
String telephone = cursor.getString(cursor.getColumnIndex(ContactsData.CONTACT_TELEPHONE));
30.
long
createDate = cursor.getLong(cursor.getColumnIndex(ContactsData.CONTACT_CREATE_DATE));
31.
Log.e(
"Test"
,
"name: "
+ name);
32.
Log.e(
"Test"
,
"telephone: "
+ telephone);
33.
Log.e(
"Test"
,
"date: "
+ createDate);
34.
35.
cursor.moveToNext();
36.
}
37.
cursor.close();
38.
}
(13)创建数据库监听器ContentObserver
在MainActivity中加入以下代码:
01.
private
ContentObserver mContentObserver =
new
ContentObserver(
new
Handler()) {
02.
03.
@Override
04.
public
void
onChange(
boolean
selfChange) {
05.
// TODO Auto-generated method stub
06.
Log.d(
"Test"
,
"mContentObserver onChange"
);
07.
super
.onChange(selfChange);
08.
}
09.
10.
};
11.
@Override
12.
protected
void
onCreate(Bundle savedInstanceState) {
13.
super
.onCreate(savedInstanceState);
14.
setContentView(R.layout.activity_main);
15.
16.
if
(savedInstanceState ==
null
) {
17.
getSupportFragmentManager().beginTransaction()
18.
.add(R.id.container,
new
PlaceholderFragment()).commit();
19.
}
20.
21.
getContentResolver().registerContentObserver(ContactsData.CONTENT_URI,
true
, mContentObserver);
22.
23.
}
每次通过insert、delete、update改变数据库内容时,都会调用ContentObserver的onChange方法,因此,可以在这个方法内做出针对数据库变化的反应,比如更新UI等。
2)、数据库的升级
当应用发布一段时间之后,我们需要改变数据库的结构,那么就需要对数据库的升级了:
将DatabaseHelper类中的DATABASE_VERSION设置为2,并且在onUpgrade函数中实现升级的代码:
01.
private
class
DatabaseHelper
extends
SQLiteOpenHelper{
02.
03.
static
final
String DATABASE_NAME =
"test.db"
;
04.
static
final
int
DATABASE_VERSION =
2
;
05.
06.
public
DatabaseHelper(Context context) {
07.
super
(context, DATABASE_NAME,
null
, DATABASE_VERSION);
08.
// TODO Auto-generated constructor stub
09.
}
10.
11.
@Override
12.
public
void
onCreate(SQLiteDatabase db) {
13.
// TODO Auto-generated method stub
14.
db.execSQL(ContactsData.SQL_CREATE_TABLE);
15.
}
16.
17.
@Override
18.
public
void
onUpgrade(SQLiteDatabase db,
int
oldVersion,
int
newVersion) {
19.
// TODO Auto-generated method stub
20.
Log.d(
"Test"
,
"onUpgrade oldVersion = "
+oldVersion+
", newVersion = "
+newVersion);
21.
//onCreate(db);
22.
for
(
int
i = oldVersion+
1
;i <= newVersion;i++){
23.
switch
(i){
24.
case
2
:
25.
db.execSQL(
"ALTER TABLE "
+ ContactsData.TABLE_NAME +
" ADD COLUMN "
+ ContactsData.CONTACT_GROUP +
" TEXT"
);
26.
break
;
27.
}
28.
}
29.
}
30.
31.
}
下面是升级前后数据库的结果:
用下面代码为DATABASE_VERSION = 2的数据库中的James设在组别和加入Howard联系人:
01.
private
void
modifyContact1(){
02.
ContentValues values =
new
ContentValues();
03.
values.put(ContactsData.CONTACT_GROUP,
"Miami"
);
04.
int
count = getContentResolver().update(ContactsData.CONTENT_URI,values, ContactsData.CONTACT_NAME+
"='James'"
,
null
);
05.
Log.d(
"Test"
,
"count = "
+count);
06.
}
07.
08.
private
void
insertContact2(){
09.
ContentValues values =
new
ContentValues();
10.
values.put(ContactsData.CONTACT_NAME,
"Howard"
);
11.
values.put(ContactsData.CONTACT_TELEPHONE,
"13333333333"
);
12.
values.put(ContactsData.CONTACT_CONTENT,
"NBA Star"
);
13.
values.put(ContactsData.CONTACT_GROUP,
"Rockets"
);
14.
values.put(ContactsData.CONTACT_CREATE_DATE, System.currentTimeMillis());
15.
Uri uri = getContentResolver().insert(ContactsData.CONTENT_URI, values);
16.
Log.d(
"Test"
,
"uri = "
+uri);
17.
}
结果如下: