22、权限与电话:CALL_PHONE、READ_PHONE_STATE、通话记录权限

电话相关的权限,说实话,是Android运行时权限里最容易踩坑的一块。

为什么?因为电话权限涉及用户隐私太深了。你的手机号、IMEI、通话记录、甚至你给谁打过电话,这些信息一旦被滥用,后果很严重。Google在这块卡得特别严,尤其是Android 10之后,政策一天比一天紧。

三个核心权限,各司其职

我们先理清楚这三个权限分别管什么。别搞混了,我见过不少开发者把CALL_PHONEREAD_PHONE_STATE当成一回事,结果审核被拒。

权限名 所属权限组 保护级别 核心能力
CALL_PHONE PHONE 危险 直接拨打电话号码
READ_PHONE_STATE PHONE 危险 读取设备标识、网络状态等
READ_CALL_LOG PHONE 危险 读取通话记录

你看,虽然都在PHONE组里,但每个权限的用途完全不同。Google在Android 10里还把READ_PHONE_STATE对IMEI的读取权限收紧了——非系统应用基本拿不到IMEI了。

CALL_PHONE:拨号权限的坑

这个权限,说白了就是让你的App能直接拨出电话,不需要经过系统拨号盘。

很多开发者图省事,直接在代码里写:

Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);

嗯,这里要注意。如果你用ACTION_CALL,就必须声明CALL_PHONE权限,并且在运行时请求。否则直接崩溃。

⚠️ 避坑指南
我曾经在一个外卖App里看到,开发者直接用ACTION_CALL拨号,结果用户拒绝权限后App直接闪退。正确的做法是:先用ACTION_DIAL(这个不需要权限),或者先检查权限再决定用哪种方式。

我个人习惯的做法是:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE)
        == PackageManager.PERMISSION_GRANTED) {
    // 有权限,直接拨号
    Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + phoneNumber));
    startActivity(intent);
} else {
    // 没权限,跳转到拨号盘
    Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
    startActivity(intent);
    // 同时请求权限,方便下次直接拨号
    requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE);
}

这样用户体验好很多。用户不会因为拒绝权限就觉得你的App有问题。

READ_PHONE_STATE:IMEI已成过去式

这个权限以前是获取IMEI的标配。但现在?基本没戏了。

Android 10开始,非系统应用调用TelephonyManager.getImei()会直接返回null。Google给的理由很充分:IMEI是永久性设备标识,不能被App追踪。

💡 我的建议
如果你的App只是需要识别设备,别再用IMEI了。改用AdvertisingIdClient(广告ID)或者InstanceId。我在项目中遇到过,因为依赖IMEI做设备指纹,结果Android 10升级后大量用户数据丢失,重构花了两周。

READ_PHONE_STATE现在还能干嘛?说实话,用处不大了。主要就是获取网络类型(4G/5G)、运营商信息这些。但即便如此,Google也在收紧——Android 11里,你甚至需要用户主动授权才能读取这些信息。

通话记录权限:隐私敏感区

READ_CALL_LOG这个权限,我建议你谨慎使用。为什么?因为它能读取用户的完整通话记录——谁打的、打了多久、什么时候打的。

你想想看,如果你的App是个拨号器或者来电拦截工具,那没问题。但如果是个游戏或者社交App,你去申请这个权限,审核基本过不了。

📌 实用技巧
如果你确实需要读取通话记录,记得在权限请求弹窗里说清楚用途。比如:「我们需要读取通话记录,用于拦截骚扰电话。」不要写「用于改善用户体验」这种空话,审核员一眼就能看出来。

读取通话记录的代码示例:

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG)
        == PackageManager.PERMISSION_GRANTED) {
    Cursor cursor = getContentResolver().query(
            CallLog.Calls.CONTENT_URI,
            null, null, null,
            CallLog.Calls.DATE + " DESC");
    if (cursor != null) {
        while (cursor.moveToNext()) {
            String number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
            long duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION));
            // 处理数据...
        }
        cursor.close();
    }
}

注意,这里用的是CallLog.Calls.CONTENT_URI,这是系统提供的通话记录内容提供者。没有权限的话,查询会直接返回空。

三个权限的请求策略

这三个权限虽然都在PHONE组里,但请求策略完全不同。我总结了一个表格:

场景 推荐权限 请求时机 备注
点击「呼叫客服」 CALL_PHONE 点击时请求 优先用ACTION_DIAL兜底
显示设备网络类型 READ_PHONE_STATE 进入相关页面时 Android 10+基本拿不到IMEI
显示最近通话 READ_CALL_LOG 首次进入通话记录页 必须说明用途

我个人建议,READ_PHONE_STATE能不用就不用。如果你的App目标API是31(Android 12)以上,这个权限甚至被标记为「仅限特殊权限」,普通应用根本拿不到。

知识体系梳理

为了让你更直观地理解这三个权限的关系和调用流程,我画了一张图:

电话权限核心逻辑图 Android 应用 CALL_PHONE READ_PHONE_STATE READ_CALL_LOG 直接拨打电话 需运行时请求 读取设备标识/网络状态 Android 10+ IMEI受限 读取通话记录 隐私敏感,审核严格 策略:按需请求 + 兜底方案 + 说明用途 拒绝权限时提供降级体验,避免崩溃

从这张图你能看出来,三个权限虽然都跟「电话」有关,但各自的目标完全不同。请求策略上,核心就一句话:按需请求,拒绝时给兜底

最后说两句

电话权限这块,Google的政策变化很快。我记得Android 8的时候还能随便拿IMEI,到了Android 11基本就封死了。所以我的建议是:尽量降低对电话权限的依赖

能用ACTION_DIAL就别用ACTION_CALL,能用广告ID就别用IMEI,能不读通话记录就别读。这样你的App在未来的Android版本里才不会突然「炸掉」。

🔑 核心要点回顾
  • CALL_PHONE:直接拨号,必须运行时请求,拒绝时用ACTION_DIAL兜底
  • READ_PHONE_STATE:读取设备状态,Android 10+ IMEI不可用,建议废弃
  • READ_CALL_LOG:读取通话记录,隐私敏感,审核严格,非必要不申请
  • 三个权限同属PHONE组,但请求策略各不相同,不要混为一谈

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