19、权限与联系人:READ_CONTACTS、WRITE_CONTACTS、联系人同步权限
联系人,说白了就是手机里的“人脉数据库”。
我做过一个通讯录备份App,当时对这块权限的理解,真的是踩坑踩出来的。今天咱们就聊聊READ_CONTACTS、WRITE_CONTACTS,还有那个容易被忽略的“联系人同步权限”。
19.1 这三个权限到底管什么?
先看一张图,理清它们的关系:
嗯,这张图基本说清楚了。READ_CONTACTS 是“读”,WRITE_CONTACTS 是“写”,同步权限则是“后台同步”的钥匙。
核心要点:READ_CONTACTS 和 WRITE_CONTACTS 属于 dangerous 级别权限,必须运行时申请。同步权限(GET_ACCOUNTS)在 Android 8.0+ 已经降级为普通权限,但涉及账户同步适配器时仍需注意。
19.2 运行时申请:代码怎么写?
我习惯用一个工具类来统一管理权限申请。直接上代码:
// 检查并申请联系人读取权限
public void requestReadContactsPermission(Activity activity) {
if (ContextCompat.checkSelfPermission(activity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 是否需要展示额外解释?
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.READ_CONTACTS)) {
// 这里可以弹个Dialog解释为什么需要这个权限
showPermissionRationaleDialog(activity);
} else {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.READ_CONTACTS},
REQUEST_CODE_READ_CONTACTS);
}
} else {
// 权限已授予,直接读取联系人
loadContacts();
}
}
你想想看,这段代码其实有个坑——shouldShowRequestPermissionRationale 在用户勾选“不再询问”后会返回 false。我曾经因为这个原因,导致用户拒绝后无法再次弹出申请,只能引导去设置里手动开启。
我的建议:在 onRequestPermissionsResult 回调中,如果权限被拒绝且 shouldShowRequestPermissionRationale 为 false,直接跳转到应用设置页,让用户手动开启。别傻等。
19.3 读取联系人:ContentResolver 的正确姿势
读取联系人,说白了就是查数据库。Android 的联系人数据存在 ContactsContract 这个 ContentProvider 里。
// 读取所有联系人姓名和电话
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(
ContactsContract.Contacts.CONTENT_URI,
null, // 查询所有列
null, // 无筛选条件
null,
ContactsContract.Contacts.DISPLAY_NAME + " ASC"
);
if (cursor != null && cursor.moveToFirst()) {
do {
String contactId = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts._ID));
String displayName = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
// 查询该联系人的电话号码
Cursor phoneCursor = resolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?",
new String[]{contactId},
null);
// 处理电话号码...
phoneCursor.close();
} while (cursor.moveToNext());
cursor.close();
}
这里要注意:联系人可能有多个号码,而且查询时一定要在子线程里做。我见过有人在主线程直接查联系人,结果 UI 卡死,被用户骂惨了。
避坑指南:我曾经在 Android 10 上遇到一个问题——后台读取联系人被系统拦截。Google 从 Android 10 开始限制后台 App 读取联系人数据。如果你的 App 在后台运行,就算有 READ_CONTACTS 权限,也会返回空数据。解决方案是使用前台服务,或者干脆等用户回到前台再读。
19.4 写入联系人:增删改的细节
写入联系人比读取复杂一些。我直接给个添加联系人的例子:
// 添加一个联系人
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// 先插入一个 RawContact
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(
ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build());
// 添加姓名
ops.add(ContentProviderOperation.newInsert(
ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,
rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"张三")
.build());
// 添加电话号码
ops.add(ContentProviderOperation.newInsert(
ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,
rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "13800138000")
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
.build());
// 批量执行
ContentResolver resolver = context.getContentResolver();
try {
resolver.applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
e.printStackTrace();
}
嗯,这里有个关键点——withValueBackReference。为什么用它?因为 RawContact 的 _ID 是自增的,你没法提前知道。用 BackReference 可以让系统自动关联。
19.5 联系人同步权限:GET_ACCOUNTS 与 SyncAdapter
同步权限,说白了就是让你的 App 能跟服务器“双向同步”联系人。
Android 提供了 SyncAdapter 框架来实现这个功能。但前提是:
- 你的 App 需要声明
GET_ACCOUNTS权限(Android 8.0+ 已降级为普通权限) - 需要创建一个
Authenticator(即使不做认证,也得有个占位) - 需要实现
AbstractThreadedSyncAdapter的子类 - 在 AndroidManifest 中注册
SyncService和SyncAdapter的 meta-data
说实话,这个框架有点重。我做过一个项目,其实只需要把联系人备份到云端,根本不需要实时同步。后来我直接用 WorkManager 定时上传,省去了 SyncAdapter 的复杂度。
我的建议:除非你真的需要“双向实时同步”(比如企业通讯录),否则别碰 SyncAdapter。用 WorkManager + 手动触发同步,开发成本低得多。
19.6 权限组合使用的常见场景
| 场景 | 需要的权限 | 说明 |
|---|---|---|
| 显示联系人列表 | READ_CONTACTS | 只读,不修改 |
| 通讯录备份 | READ_CONTACTS + WRITE_CONTACTS | 读取后写入本地文件或云端 |
| 通讯录恢复 | WRITE_CONTACTS | 从备份文件写入系统 |
| 企业通讯录同步 | READ_CONTACTS + WRITE_CONTACTS + GET_ACCOUNTS | 需要 SyncAdapter 支持 |
| 来电识别 | READ_CONTACTS | 匹配来电号码 |
19.7 避坑总结
最后,我把自己踩过的坑列一下:
- 权限被撤销:Android 11+ 用户如果一段时间不用你的 App,系统会自动撤销权限。记得在 onResume 里重新检查权限状态。
- 联系人数据格式:不同厂商的联系人数据可能略有差异。比如小米手机的联系人可能有额外的自定义字段。别假设所有手机都一样。
- 批量操作性能:用
applyBatch而不是逐条插入。我试过逐条插入 1000 个联系人,耗时 30 秒;用 applyBatch 只要 2 秒。 - 权限申请时机:不要在 App 启动时一股脑申请所有权限。用户会反感。最好在用到某个功能时再申请,比如用户点击“导入联系人”时。
好了,关于联系人权限,今天就聊这么多。记住一句话:权限是用户对你的信任,别滥用。
公众号:蓝海资料掘金营,微信deep3321