ContentProvider之读写联系人、短信、通话记录

原理

每个应用程序的数据都是自己私有的,一般不会开放公有权限,但是当应用的使用规模较大,并衍生出其他以此为基础开发的应用,那么就需要为额外的应用开放一些权限,分享自己的一些数据供二次开发或使用。而 ContentProvider 的原理就是在自己的应用内部开放一个接口供其它应用调用,同时为了保证安全性,使用该接口需要进行安全验证。

其中接口的使用是基于 sqlite 的增删改查方法实现的。

在使用 ContentProvider 读写数据之前,先来看如何定义一个数据开放接口,之后我们使用起来就会更加清晰有条理。

实现 ContentProvider 开放接口

假设该应用本地已经建立一个数据库名为 data.db,数据库中有一张名为 user 的表。

  1. 新建一个类MyDbProvider继承ContentProvider,该类默认实现数据库操作的四个方法,也就是增、删、改、查。同时还有两个额外的方法: onCreate()getType(Uri uri),前者大家都很熟悉,是该对象被创建时执行的方法,后者我们暂且不提,后面会讲到。(暂且不去实现增删改查的逻辑)

  2. 在清单文件中配置:
    <provider name=".MyDbProvider" authority="com.alpha.db" android:exported="true">
    其中authority里写你自己定义的主机名,一般为自己 app 的包名,但也可以自由命名,如 google 的通话记录分享者主机名为 call_logexported="true"是 Android6.0 开始必须添加的属性,代表该组件可以被外部访问。

  3. 新建一个静态UriMatcher类,用于设置接口的匹配规则。传入的参数代表匹配失败返回-1,是固定的用法。

    1
    private static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
  1. 添加匹配规则(不必在 UriMatcher 类中添加):

    1
    2
    3
    4
    static {
    uriMatcher.addURI("com.alpha.db","user",User_ALL);
    uriMatcher.addURI("com.alpha.db","user/#",User_Single);
    }

    第一个参数为在清单文件中定义的主机名(同时也是 uri 的前缀),第二参数为匹配规则(null代表匹配所有),第三参数为自定义的匹配后返回值,必须为正整数。

    例如:
    "content://com.alpha.db/user" 表示匹配user表中所有数据 设定返回值为1
    "content://com.alpha.db/user/#" 表示匹配user表中的某一行数据 设定返回值为2

  2. 实现 MyDbProvider 中的增删改查方法

    具体的数据库操作逻辑这里不提;在进行数据库操纵之前,应该对得到的 Uri 进行匹配,是否与已定义的匹配类型吻合,若吻合则继续执行,否则应该抛出匹配失败的异常。

    同时在前面提到的 getType(Uri uri) 方法里写入以下逻辑:(按需求在数据操纵方法中添加适当判断)

    1
    2
    3
    4
    5
    6
    7
    8
    int code =uriMatcher.match(uri);
    if (code==User_ALL){
    return "vnd.android.cursor.dir/";//"vnd.android.cursor.dir/"表示该路径为一个目录
    }else if (code==User_Single){
    return "vnd.android.cursor.item/user";//"vnd.android.cursor.item/..."表示该路径为一个行或项
    }else {
    throw new IllegalArgumentException("匹配失败");
    }

通过以上五步就定义好了一个 ContentProvider 开放接口,接下来就是在外部应用中使用这个接口了。

使用ContentProvider开放接口

  1. 获取 ContentProvider 的解析器 ContentResolver:

    ContentResolver resolver = getContentResolver();

  2. 设置需要操作的数据库的路径:

    Uri uri = Uri.parse("content://com.alpha.db/user");

    以上为获取user表的全部数据,若只需要某一行,则为(12表示行数):

    Uri uri = Uri.parse("content://com.alpha.db/user/12");

  3. 使用 resolver 的增删改查方法就可以对目标数据表进行操作

    通过 resolver 的 query() 方法会返回一个 Cursor 对象,我理解为通过 sql 语句生成的游标在本地的引用,既然有了 Cursor 对象,那么 Cursor 中的数据你也是了然于胸了吧,毕竟 sql 语句可是你自己定义的。

    同样,resolver 的 insert() 方法会返回一个 uri,表示插入数据在该表中的位置;
    update()delete() 会返回一个 int 数据,表示操作影响了几行。

    另外还可以通过 resolver 的 getType() 方法得到路径的类型,你可以分别对不同的路径类型执行不同的操作。

通过 ContentProvider 读写手机联系人

首先必须知道手机联系人的 ContentProvider 的主机名,表名,如果你看过 Android 上层应用的源码,你应该知道 Android 原生应用的联系人开放主机名为“com.android.contacts”,但是表就稍有点复杂,存储联系人涉及到三张表:

  • raw_contacts 保存联系人的 id,每一个联系人都有一个不同的id,名字叫 contact_id

  • data 保存联系人的数据,通过 raw_contact_id 来去识别这个数据属于哪个联系人

  • mimetype 保存数据的类型

查询联系人信息的步骤.

  1. 查询 raw_contacts 表, 得到每个联系人的 contact_id.
  2. 根据 contact_id 查询 data 表,把联系人的数据取出来.
  3. 根据联系人数据的 mimetype,获取数据代表什么含义.

原理明白了,下面直接上代码:(在这里我只查询 data 表中的 data1 列,和 mimetype 列)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ContactsUtils {
public static List<ContactsInfo> getAllContactInfo(Context context){
ContentResolver resolver=context.getContentResolver();
Uri uri=Uri.parse("content://com.android.contacts/raw_contacts");
Cursor cursor=resolver.query(uri,new String[]{"contact_id"},null,null,null);
List <ContactsInfo> contactsInfoList=new ArrayList<>();
if (cursor!=null){
while (cursor.moveToNext()){
String id=cursor.getString(0);
if (!TextUtils.isEmpty(id)){
ContactsInfo info = new ContactsInfo();
Uri datauri=Uri.parse("content://com.android.contacts/data");
Cursor dataCursor=resolver.query(datauri,new String[]{"data1","mimetype"}
,"raw_contact_id=?"
,new String[]{id},null);
if (dataCursor!=null){
while (dataCursor.moveToNext()){
String data1=dataCursor.getString(0);
String mimetype=dataCursor.getString(1);
if ("vnd.android.cursor.item/phone_v2".equals(mimetype)){
info.setNumber(data1);
}else if ("vnd.android.cursor.item/email_v2".equals(mimetype)){
info.setEmail(data1);
}else if ("vnd.android.cursor.item/name".equals(mimetype)){
info.setName(data1);
}else if ("vnd.android.cursor.item/im".equals(mimetype)){
info.setQq(data1);
}
}
dataCursor.close();
contactsInfoList.add(info);
}
}
}
cursor.close();
}
return contactsInfoList;
}
}

联系人实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ContactsInfo {
private String name;
private String number;
private String email;
private String qq;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getQq() {
return qq;
}
public void setQq(String qq) {
this.qq = qq;
}
@Override
public String toString() {
return "[name="+name+",number="+number+",email="+email+",qq="+qq+"]";
}
}

之后需要查询联系人就只需要一行代码:

1
List<ContactsInfo> contactsInfoList= ContactsUtils.getAllContactInfo(this);

注意
如果是 Android6.0,则最好先获得运行时权限:

1
2
3
4
5
6
//6.0需要申请运行时权限
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_CONTACTS}, 1);
}

写入联系人的步骤

因为涉及到3张表的关系,我们需要分别插入几张表,最后达到插入一个完整的联系人的目的。

  1. 在 raw_contacts 表里面添加联系人的 id。
  2. 根据这个添加的 id,在 data 表里面添加联系人的数据。

具体代码如下:(你可以把 info 对象替换为通过用户输入获取的信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void addContact(View view) {
ContactsInfo info=new ContactsInfo();
info.setName("alphagao");
info.setEmail("alphagao@github.com");
info.setNumber("12345678910");
info.setQq("123456789");
//利用内容提供者添加联系人的数据到系统的数据库内部
Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
Uri dataUri = Uri.parse("content://com.android.contacts/data");
ContentResolver resolver = getContentResolver();
//1. 在raw_contact表里面添加联系人的id
Cursor cursor = resolver.query(uri, null, null, null, null);
int new_id = cursor.getCount()+1;
ContentValues values = new ContentValues();
values.put("contact_id", new_id);
resolver.insert(uri, values);
//2. 在data表里面添加联系人的数据.
ContentValues namevalues = new ContentValues();
namevalues.put("raw_contact_id", new_id);
namevalues.put("data1", info.getName());
namevalues.put("mimetype", "vnd.android.cursor.item/name");
resolver.insert(dataUri, namevalues);

ContentValues phonevalues = new ContentValues();
phonevalues.put("raw_contact_id", new_id);
phonevalues.put("data1", info.getNumber());
phonevalues.put("mimetype", "vnd.android.cursor.item/phone_v2");
resolver.insert(dataUri, phonevalues);

ContentValues emailvalues = new ContentValues();
emailvalues.put("raw_contact_id", new_id);
emailvalues.put("data1", info.getEmail());
emailvalues.put("mimetype", "vnd.android.cursor.item/email_v2");
resolver.insert(dataUri, emailvalues);

Toast.makeText(this, "添加信息成功", Toast.LENGTH_SHORT).show();
}

如图:
添加联系人成功示例

读写通话记录和短信

其实如果你已经明白和理解读写联系人,那么通话记录就是小菜一碟,毕竟通话记录是存在一张表中的。所以这里不再放具体的读写代码,仅放出对应的路径和表名。

  • 通话记录 URI:"content://call_log/calls" 其中“call_log”为主机名,前文已经提过,calls为匹配规则,表示查询所有数据,关键数据列有”number”,”date”,”type”,分表表示号码、日期、呼叫类型;
  • 短信 URI:"content://sms/"这里没有匹配规则,代表查询表中所有数据,包含发送的,接受的,草稿,发送失败等等。。关键数据列有”address”,”date”,”type”,”body”,分别表示对方号码,日期,短信类型(发送,接受,草稿,发送失败),以及短信内容。

以上读写联系人、短信、通话记录仅适用于 google 原生应用,第三方 rom 因为定制了出厂应用,内容提供者已经不再是原生的了。