18、联系人权限管理:运行时权限请求流程、处理权限拒绝、权限最佳实践、Android 11包可见性影响

权限管理,说白了就是你的App跟系统之间的一场「信任博弈」。你问系统要权限,系统不一定给,用户也不一定乐意。我做了这么多年联系人开发,可以说权限这块是踩坑最多的环节之一。今天咱们就把运行时权限、拒绝处理、最佳实践,还有Android 11那个让人头疼的包可见性,一次性捋清楚。

运行时权限请求流程:别让用户一头雾水

Android 6.0之后,危险权限都得运行时申请。联系人相关的权限主要是READ_CONTACTSWRITE_CONTACTS。我个人习惯是把权限请求封装成一个独立的模块,这样代码清爽,也好维护。

基本流程其实就三步:

  1. 检查权限:调用ContextCompat.checkSelfPermission(),看看用户有没有给。
  2. 请求权限:如果没给,调用ActivityCompat.requestPermissions(),弹出系统对话框。
  3. 处理结果:在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必须优雅地处理拒绝。

我把拒绝场景分成两种:

  • 临时拒绝:用户点了「拒绝」,但没勾选「不再询问」。下次请求时系统还会弹窗。
  • 永久拒绝:用户点了「拒绝并不再询问」。系统不会再弹窗,你只能引导用户去设置。

处理方式也很明确:

  1. onRequestPermissionsResult()里,如果grantResults包含PERMISSION_DENIED,先判断shouldShowRequestPermissionRationale()
  2. 如果返回true,说明是临时拒绝。你可以再次弹出自定义解释框,给用户一个台阶下。
  3. 如果返回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+ 跳转系统联系人

从图里可以看出来,权限请求是一个闭环。用户拒绝后,我们要根据拒绝类型做不同处理。而包可见性则像一个「隐形门槛」,在Android 11上如果你忘了声明,跳转功能就会失效。

我的个人建议:在开发联系人相关功能时,最好在项目的BuildConfig里加一个MIN_SDK_VERSION判断。如果Build.VERSION.SDK_INT >= Build.VERSION_CODES.R,就动态检查包可见性声明是否齐全。我习惯写一个工具方法,专门用来检测当前设备上目标App是否可见,这样调试起来方便很多。

好了,权限管理这块内容不少,但核心就是「尊重用户、清晰解释、优雅降级」。你把这些原则落实到代码里,用户用着舒心,你的App也不会因为权限问题被差评。


公众号:蓝海资料掘金营,微信deep3321