22、权限与电话:CALL_PHONE、READ_PHONE_STATE、通话记录权限
电话相关的权限,说实话,是Android运行时权限里最容易踩坑的一块。
为什么?因为电话权限涉及用户隐私太深了。你的手机号、IMEI、通话记录、甚至你给谁打过电话,这些信息一旦被滥用,后果很严重。Google在这块卡得特别严,尤其是Android 10之后,政策一天比一天紧。
三个核心权限,各司其职
我们先理清楚这三个权限分别管什么。别搞混了,我见过不少开发者把CALL_PHONE和READ_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)以上,这个权限甚至被标记为「仅限特殊权限」,普通应用根本拿不到。
知识体系梳理
为了让你更直观地理解这三个权限的关系和调用流程,我画了一张图:
从这张图你能看出来,三个权限虽然都跟「电话」有关,但各自的目标完全不同。请求策略上,核心就一句话:按需请求,拒绝时给兜底。
最后说两句
电话权限这块,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组,但请求策略各不相同,不要混为一谈