1.ContentProvider是什么
ContentProvider从字面上理解是内容提供器的意思,主要是用来实现程序与程序之间实现数据共享的功能,在Android中提供了一套完整的机制,它允许一个程序去访问另一个程序中的数据,同时还能保证数据的安全性。ContentProvider可以选择只对哪一部分的数据进行共享,不进行共享的数据其他的程序就无法访问,从而保证了程序中重要的信息不会泄漏。
ContentProvider的用法一般有两种,一种是使用现有的内容提供器来读取相应程序中的数据,另一种是自定义自己的内容提供器并且选择好要共享的数据,提供给其他程序外部访问的接口。
2.访问其它程序中的数据
在做开发的大多数时候用到ContentProvider并不是要共享自己程序中的数据,而是要去访问其它程序中的数据,最常用的功能就是读取手机中的联系人,短信等。Android系统中自带的电话簿、短信和媒体库等应用都为我们提供了类似的访问接口,这就使得第三方应用可以利用这些数据来实现更好的功能。
1)读取收件箱信息
//获取收件箱的哪些信息
public List<SMSContent> getSMS()
{
String[] projection = new String[]
{
"address",
"body",
"type",
"date"
};
try
{
Uri uri = Uri.parse("content://sms/");
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor.moveToFirst())
{
String address;
String body;
String date;
String type;
int phoneColumn = cursor.getColumnIndex("address");
int bodyColumn = cursor.getColumnIndex("body");
int dateColumn = cursor.getColumnIndex("date");
int typeColumn = cursor.getColumnIndex("type");
while(cursor.moveToNext())
{
SMSContent content = new SMSContent();
address = cursor.getString(phoneColumn);
body = cursor.getString(bodyColumn);
date = cursor.getString(dateColumn);
type = cursor.getString(typeColumn);
content.setAddress(address);
content.setBody(body);
content.setDate(date);
content.setType(type);
SMSList.add(content);
}
cursor.close();
}
}
catch (Exception e)
{
e.printStackTrace();
}
return SMSList;
}
此时还要在AndroidManifest.xml中加入读取收件箱的权限
<uses-permission android:name="android.permission.READ_SMS"/>
由上面的代码可以看出在得到收件箱URI地址后,通过ContentResolver的query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder)的方法可以获取收件箱的全部信息。—uri:ContentProvider需要返回的资源索引
—projection:用于标识有哪些columns需要包含在返回数据中。例如:id号,地址,消息体,读取状态等
—selection:做为查询符合条件的过滤参数
—selectionArgs:过滤参数的数据
—sortOrder:对于返回信息进行排列
关于短信的columns分别为:
—_id//短消息序号 如100
—thread_id//对话的序号 如100
—address//发件人地址,手机号.如+8613811810000
—person//发件人,返回一个数字就是联系人列表里的序号,陌生人为null
—date//日期 long型。如1256539465022
—protocol//协议 0 SMS_RPOTO, 1 MMS_PROTO
—read//是否阅读 0未读, 1已读
—status//状态 -1接收,0 完成, 64 未接收, 128 失败
—type//类型 1是接收到的,2是已发出
—body//短消息内容
—service_center //短信服务中心号码编号。如+8613800755500
关于短信的URI和数据库表的字段分别为:
—String SMS_URI_ALL = "content://sms/"; //全部
—String SMS_URI_INBOX = "content://sms/inbox";//收件箱
—String SMS_URI_SENT = "content://sms/sent"; //已发送
—String SMS_URI_DRAFT = "content://sms/draft";//草稿箱
—String SMS_URI_OUTBOX = "content://sms/outbox";//发件箱
—String SMS_URI_FAILED = "content://sms/failed"; //发送失败
—String SMS_URI_QUEUED = "content://sms/queued";//待发送列表
query()的返回值是一个Cursor的对象,我们查询的数据都在Cursor的对象中,只需要将数据读出即可。
2)在收件箱中插入一条信息
其实在收件箱中插入一条信息就是通过ContentResolver直接向数据库中写入信息
//插入一条信息到收件箱中
/*
address:发件人地址(电话号码)
type:信息类型(1是接收到的,2是已发出)
date:日期
body:/短消息内容
*/
public boolean insertSMS(String address, int type, long date, String body)
{
try
{
Uri uri = Uri.parse("content://sms/");
ContentResolver contentResolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put("address", address);
contentValues.put("type", type);
contentValues.put("date", date);
contentValues.put("body", body);
contentResolver.insert(uri, contentValues);
return true;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
此时还要在AndroidManifest.xml中加入写入收件箱的权限
<uses-permission android:name="android.permission.WRITE_SMS"/>
由上面的代码可以看出在得到收件箱的URI地址后通过ContentResolver的insert(Uri url,ContentValues values)的方法可以往收件箱中插入一条信息,ContentValues是一种存储机制,使用put()方法和get()方法可以分别存入和取出数据,ContentValues只能存储基本类型的数据,像string、int之类的,不能存储对象这种东西。
3)读取手机通讯录
跟读取收件箱相似,都是通过query()方法进行查询
//读取通讯录
public List<Contacts> getContact()
{
try
{
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
//使用ContentResolver查找联系人数据
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
//遍历查询结果,获取系统中所有的联系人
if(cursor.moveToFirst())
{
String number;
String displayName;
int displayNameColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
int numberColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
while (cursor.moveToNext())
{
contacts = new Contacts();
//获取联系人姓名
displayName = cursor.getString(displayNameColumn);
//获取联系人手机号码
number = cursor.getString(numberColumn);
contacts.setName(displayName);
contacts.setNumber(number);
contactsList.add(contacts);
}
}
cursor.close();
}
catch (Exception e)
{
e.printStackTrace();
}
return contactsList;
}
此时还要在AndroidManifest.xml中加入读取联系人的权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
4)添加一个新的联系人
//插入联系人到通讯录
public boolean insertContact(String name, String number)
{
try
{
Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
Uri dataUri = Uri.parse("content://com.android.contacts/data");
ContentResolver contentResolver = context.getContentResolver();
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
ContentProviderOperation op1 = ContentProviderOperation.newInsert(uri)
.withValue("account_name", null)
.build();
operations.add(op1);
//加入姓名
ContentProviderOperation op2 = ContentProviderOperation.newInsert(dataUri)
.withValueBackReference("raw_contact_id", 0)
.withValue("mimetype", "vnd.android.cursor.item/name")
.withValue("data2", name)
.build();
operations.add(op2);
//加入电话号码
ContentProviderOperation op3 = ContentProviderOperation.newInsert(dataUri)
.withValueBackReference("raw_contact_id", 0)
.withValue("mimetype", "vnd.android.cursor.item/phone_v2")
.withValue("data1", number)
.withValue("data2", "2")
.build();
operations.add(op3);
//将上述内容添加到电话号码中
contentResolver.applyBatch("com.android.contacts", operations);
return true;
}
catch(Exception e)
{
return false;
}
}
在新增联系人中使用到了ContentProviderOperation这样的类,Android系统中引入这个类是为了通ContentResolver的对应函数批量更新、插入、删除数据时更加方便,在开发文档中推荐使用ContentProviderOperation的原因为:
- 所有的操作都在一个事务中执行,这样可以保证数据完整性
- 由于批量操作在一个事务中执行,只需要打开和关闭一个事务,比多次打开关闭多个事务性能要好些
- 使用批量操作和多次单个操作相比,减少了应用和content
- provider之间的上下文切换,这样也会提升应用的性能,并且减少占用CPU的时间,当然也会减少电量的消耗。
- newInsert 创建一个用于执行插入操作的Builder
- newUpdate 创建一个用于执行更新操作的Builder
- newDelete 创建一个用于执行删除操作的Builder
3.自定义ContentProvider
自定义ContentProvider提供该应用程序的URI,将本程序的数据提供给第三方应用。这种情况在实际的开发中很少遇到,因为我们都不希望将自己的数据提供给其它APP使用,但是学会怎样使用自定义ContentProvider还是很有必要的。
1)首先需要新建一个类,继承自ContentProvider
package com.example.administrator.contentproviderdemo;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
public class MyContentProvider extends ContentProvider
{
//初始化
public boolean onCreate()
{
return false;
}
//查询
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
{
return null;
}
//插入
public Uri insert(Uri uri, ContentValues values)
{
return null;
}
//删除
public int delete(Uri uri, String selection, String[] selectionArgs)
{
return 0;
}
//更新
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
{
return 0;
}
//得到MIME类型
public String getType(Uri uri)
{
return null;
}
}
在使用子类继承
ContentProvider时,需要重写以上六个方法,接下来简单介绍一下这六个方法的作用
- onCreate()—初始化ContentProvider的时候调用,只会调用一次。通常在这里完成数据库的创建和升级操作,只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化。
- query()—查询数据时调用,uri用于确定查询哪张表,projection用于确定查询哪几列,selection和selectionArgs用于约束查询哪些行,sortOrder用于对查询到的结果进行排序。在查询成功后会将查询结果放在一个Cursor的对象中并返回。
- insert()—插入数据时调用,uri用于确定插入数据到哪张表,values用于封装需要添加的数据。插入成功以后将会返回一个用于表示新纪录的URI。
- update()—更新数据时调用,uri用于确定更新数据到哪张表,values用于封装需要更新的数据,selection和selectionArgs用于约束更新哪些行。更新成功以后会返回受影响的行数。
- delete()—删除数据时调用,uri用于确定删除数据从哪张表,selection和selectionArgs用于约束删除哪些行。删除成功以后会返回被删除的行数。
- getType()—返回MIME类型时调用,根据传入的内容URI来返回相应的MIME类型。
2)接下来在使用URI对哪些表中的哪些数据进行操作之前,还要对传入的URI进行匹配,由于ContentProvider可以提供我们想提供的数据,不想提供的数据可以不提供。
要实现数据筛选就需要借助UriMatcher这个类实现URI匹配内容的功能,我们主要要使用这个类中的addURI()和match()方法。
addURI(String authority, String path, int code)—添加一个URI匹配
- authority—权限匹配
- path—路径匹配
- code—自定义代码
match(Uri uri)—匹配URI的路径,返回一个能够匹配URI对象所对应的自定义代码,根据这个代码判断出调用方期望访问哪张表中的数据
- uri—uri路径
//查询
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
{
switch (uriMatcher.match(uri))
{
case TABLE1_DIR:
break;
case TABLE1_ITEM:
break;
case TABLE2_DIR:
break;
case TABLE2_ITEM:
break;
default:
break;
}
return null;
}
当query()方法被调用的时候,首先会通过UriMatcher的match()方法对传入的Uri进行匹配,如果发现UriMatcher中某个内容URI成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的数据是什么,再进行相关的操作。如果没有匹配成功则说明传入的Uri错误或者调用方期望访问的数据没有开放。
//得到MIME类型
public String getType(Uri uri)
{
switch (uriMatcher.match(uri))
{
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.example.administrator.contentproviderdemo.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.example.administrator.contentproviderdemo.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.example.administrator.contentproviderdemo.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.example.administrator.contentproviderdemo.table2";
default:
break;
}
return null;
}
一个内容URI对应的MIME字符串主要由三部分组成,Android对这三个部分做了如下格式规定:
- 必须以vnd开头
- 如果内容URI以路径结尾则后接android.cursor.dir/,如果内容URI以id结尾则后接android.cursor.item/
- 最后接上vnd.<authority><path>
<provider
android:authorities="com.example.administrator.contentproviderdemo"
android:name="com.example.administrator.contentproviderdemo.MyContentProvider"
android:exported="true"/>
注册的属性依次为用于匹配URI的权限名,类名和是否共享数据
4.通过ContentObserver监听ContentProvider的数据变化
ContentObserver—内容观察者,目的是观察特定Uri引起数据库变化而做出相应的处理。在使用ContentObserver的过程中需要自定一个类并且继承自ContentObserver并且重写onChange()方法
在这里我们要监听用户发短信的状态,此时需要知道短信的Uri分别为以下六种:
- content://sms/inbox—收件箱
- content://sms/sent—已发送
- content://sms/draft—草稿
- content://sms/outbox—发件箱(正在发送的信息)
- content://sms/failed—发送失败
- content://sms/queued—待发送列表(在开启飞行模式后,短信就在待发送列表中)
package com.example.administrator.contentproviderdemo;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;
/**
* Created by ChuPeng on 2017/1/4.
*/
public class MyContentObserver extends ContentObserver
{
/**
* Creates a content observer.
*
* @param handler The handler to run {@link #onChange} on, or null if none.
*/
private Context context;
public MyContentObserver(Handler handler, Context context)
{
super(handler);
this.context = context;
}
/**
* 当监听的Uri发生改变时,就会回调此方法
*
* @param selfChange 一般情况下为false,意义不是很大
*/
public void onChange(boolean selfChange)
{
//查询发件箱中的短信
Uri uri = Uri.parse("content://sms/outbox");
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if(cursor != null)
{
//对查询结果进行遍历
while(cursor.moveToNext())
{
StringBuilder sb = new StringBuilder();
//发送地址
sb.append("address=").append(cursor.getString(cursor.getColumnIndex("address"))).append("; ");
//短信内容
sb.append("body=").append(cursor.getString(cursor.getColumnIndex("body"))).append("; ");
//是否查看
sb.append("read=").append(cursor.getString(cursor.getColumnIndex("read"))).append("; ");
//发送时间
sb.append("time=").append(cursor.getString(cursor.getColumnIndex("date"))).append("\n");
Log.d("sms", sb.toString());
}
//在使用完以后需要关闭
cursor.close();
}
else
{
Toast.makeText(context, "查询失败", Toast.LENGTH_SHORT).show();
}
}
}
在完成自定义的ContentObserver以后,想要进行监听还需要使用registerContentObserver()方法在相应的Activity中去进行注册
registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer)
- uri—需要监听的Uri(Uri已经被添加到UriMatcher)
- notifyForDescendents—是布尔型的常量,为true时代表精确匹配,为false时代表模糊匹配(可以匹配派生出的Uri)
- observer—ContentObserver的对象实例
- observer—ContentObserver的对象实例
//监听ContentProvider的数据变化
MyContentObserver myContentObserver = new MyContentObserver(new Handler(), MainActivity.this);
getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, myContentObserver);
运行程序以后,在发送短信时可以在logcat中看到该条信息的内容,在实际开发中我们可以根据自己的需求在Service中开启监听,这样就可以在后台监听ContentProvider的数据变化。
5.总结
在实际开发中使用ContentObserver主要有一下两种情况:
- 需要频繁的监听数据库或者某个数据是否发生改变,这种情况下如果使用线程去操作就变得很耗时
- 在用户不知道的情况下对数据库做一些改变,比如在后台悄悄发送短信等
需要注意的一点是,如果我们使用ContentObserver监听一个自定义的ContentProvider,当自定义的ContentProvider数据发生改变时,却监听不到任何反应。这是因为在每个ContentProvider数据源发生改变时,如果想通知其监听对象就必须在对应的方法中(query()、insert()、update()和delete())调用this.getContentReslover().notifychange(uri, observer)方法,用来通知监听的ContentObserver,否则ContentObserver时不会监听到数据发生改变的。
notifyChange(Uri uri,ContentObserver observer)
uri—需要监听的Uri
observer—监听数据的ContentObserver对象,可以为null
以上Demo的源代码地址:点击打开链接