18、联系人权限管理:运行时权限请求流程、处理权限拒绝、权限最佳实践、Android 11包可见性影响
权限管理,说白了就是你的App跟系统之间的一场「信任博弈」。你问系统要权限,系统不一定给,用户也不一定乐意。我做了这么多年联系人开发,可以说权限这块是踩坑最多的环节之一。今天咱们就把运行时权限、拒绝处理、最佳实践,还有Android 11那个让人头疼的包可见性,一次性捋清楚。
运行时权限请求流程:别让用户一头雾水
Android 6.0之后,危险权限都得运行时申请。联系人相关的权限主要是READ_CONTACTS和WRITE_CONTACTS。我个人习惯是把权限请求封装成一个独立的模块,这样代码清爽,也好维护。
基本流程其实就三步:
- 检查权限:调用
ContextCompat.checkSelfPermission(),看看用户有没有给。 - 请求权限:如果没给,调用
ActivityCompat.requestPermissions(),弹出系统对话框。 - 处理结果:在
onRequestPermissionsResult()回调里,根据用户的选择做后续操作。
嗯,这里有个细节很多人会忽略:你应该先判断是否需要展示 rationale(解释说明)。如果用户之前拒绝过一次,系统会建议你弹个自定义对话框,告诉用户为什么需要这个权限。用ActivityCompat.shouldShowRequestPermissionRationale()就能判断。
核心代码片段:
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 需要展示解释吗?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS)) {
// 弹出自定义对话框,说明为什么需要读取联系人
showRationaleDialog();
} else {
// 直接请求
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS},
REQUEST_CODE_READ_CONTACTS);
}
} else {
// 权限已授予,直接读取联系人
loadContacts();
}
我在项目中遇到过一种情况:用户点了「拒绝并不再询问」,然后shouldShowRequestPermissionRationale()会返回false。这时候你再弹系统对话框也没用,因为系统不会再弹了。正确的做法是引导用户去设置里手动开启权限。这个后面会细说。
处理权限拒绝:别让App直接崩溃
用户拒绝权限太正常了。你想想看,一个陌生人突然要读你的通讯录,换你你也得犹豫一下。所以我们的App必须优雅地处理拒绝。
我把拒绝场景分成两种:
- 临时拒绝:用户点了「拒绝」,但没勾选「不再询问」。下次请求时系统还会弹窗。
- 永久拒绝:用户点了「拒绝并不再询问」。系统不会再弹窗,你只能引导用户去设置。
处理方式也很明确:
- 在
onRequestPermissionsResult()里,如果grantResults包含PERMISSION_DENIED,先判断shouldShowRequestPermissionRationale()。 - 如果返回true,说明是临时拒绝。你可以再次弹出自定义解释框,给用户一个台阶下。
- 如果返回false,说明是永久拒绝。这时候别犹豫,直接弹个对话框,告诉用户「请到设置里开启权限」,最好能提供一个「去设置」的按钮。
避坑指南:我曾经在永久拒绝后直接跳转到系统设置页,结果用户转了一圈回来,App没做任何状态刷新,导致界面还是灰的。正确的做法是在onActivityResult()里重新检查权限,或者干脆在onResume()里检查。我个人的习惯是在onResume()里做一次权限状态同步,这样用户从设置回来,界面能自动更新。
权限最佳实践:少即是多
关于权限,我有一条铁律:只申请你真正需要的权限,而且要在用户最需要的时候申请。别一启动App就噼里啪啦弹一堆权限框,用户大概率会全部拒绝。
具体来说,有这几个实践要点:
- 时机要对:比如用户点击「添加联系人」按钮时,再申请
WRITE_CONTACTS。而不是在登录页就申请。 - 解释要清:申请前用一两句话告诉用户为什么需要这个权限。比如「为了帮你把新朋友存到通讯录里,需要写入权限」。
- 拒绝要兜底:用户拒绝后,App不能直接罢工。可以降级处理,比如只显示本地缓存的数据,或者提示用户手动输入。
- 权限分组:如果同时需要读写权限,可以一起申请。但别把不相关的权限混在一起,比如把相机和联系人放在一起申请,用户会觉得很奇怪。
警告:千万不要在后台线程里检查或请求权限。所有权限相关操作都必须在UI线程进行。我曾经见过有人在Service里直接调用checkSelfPermission(),结果在某些机型上直接crash。嗯,血的教训。
Android 11包可见性影响:你的App可能「看不见」联系人App
Android 11(API 30)引入了一个大变化:包可见性(Package Visibility)。简单说,如果你的App想查询手机上安装了哪些应用,或者想启动某个特定的App(比如系统联系人App),你得在AndroidManifest.xml里声明<queries>元素。
这对联系人开发有什么影响呢?
举个例子:你想判断系统联系人App是否存在,然后引导用户去那里添加联系人。在Android 10及以下,你直接调用getPackageManager().getPackageInfo("com.android.contacts", 0)就行。但在Android 11上,如果你没有声明查询权限,这个方法会直接抛异常或者返回null。
解决方案就是在manifest里加上:
<queries>
<package android:name="com.android.contacts" />
</queries>
或者更通用的方式,如果你需要查询所有应用:
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
但我不建议用后者,因为这会暴露你的App查询了所有应用,用户可能会觉得隐私被侵犯。最好只声明你真正需要查询的那个包。
包可见性对联系人权限的影响:
其实包可见性和运行时权限是两回事。运行时权限控制的是你能不能读写联系人数据,而包可见性控制的是你能不能「看到」联系人App本身。但两者在实际开发中经常交织在一起。比如你申请了READ_CONTACTS权限,但如果你没有声明包可见性,你可能无法通过Intent跳转到系统联系人编辑页面。用户会觉得很奇怪:「明明给了权限,为什么点编辑没反应?」
我记得有一次,用户反馈说点击「编辑联系人」按钮后,App闪了一下又回来了。查了半天,发现是Android 11上startActivity()找不到处理该Intent的Activity,因为没有声明包可见性。加上<queries>声明后,问题就解决了。
知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到权限请求、拒绝处理、包可见性这三者之间的关系。
从图里可以看出来,权限请求是一个闭环。用户拒绝后,我们要根据拒绝类型做不同处理。而包可见性则像一个「隐形门槛」,在Android 11上如果你忘了声明,跳转功能就会失效。
我的个人建议:在开发联系人相关功能时,最好在项目的BuildConfig里加一个MIN_SDK_VERSION判断。如果Build.VERSION.SDK_INT >= Build.VERSION_CODES.R,就动态检查包可见性声明是否齐全。我习惯写一个工具方法,专门用来检测当前设备上目标App是否可见,这样调试起来方便很多。
好了,权限管理这块内容不少,但核心就是「尊重用户、清晰解释、优雅降级」。你把这些原则落实到代码里,用户用着舒心,你的App也不会因为权限问题被差评。