17、电话与通讯:蓝牙电话 HFP、CarPhoneService、通话界面开发
电话功能,在车载系统里是个硬需求。你想想看,用户开车时不可能举着手机打电话,那太危险了。所以 Android Automotive OS 必须把手机的通话能力无缝衔接到车机上。这一章,我们就来聊聊蓝牙电话 HFP 协议、CarPhoneService 这个核心服务,以及怎么开发一个像样的通话界面。
17.1 蓝牙 HFP 协议:车机与手机的“电话线”
HFP,全称 Hands-Free Profile,是蓝牙里专门用来做免提通话的协议。说白了,它定义了车机怎么当“耳机”,手机怎么当“交换机”。
我个人习惯把 HFP 分成两个角色:
- AG(Audio Gateway):音频网关,通常是手机。它负责收发语音、管理通话状态。
- HF(Hands-Free):免提设备,就是我们的车机。它负责播放语音、采集麦克风声音、发送按键指令。
HFP 协议里最重要的几个操作,我列一下:
| AT 指令 | 含义 | 我在项目中遇到的坑 |
|---|---|---|
| AT+CHUP | 挂断电话 | 有些手机不响应这个指令,得用 AT+CHLD=1 |
| AT+CLCC | 查询当前通话列表 | 返回格式因手机而异,解析时一定要做容错 |
| AT+BLDN | 重拨上次号码 | 如果上次通话记录被删了,手机会直接忽略 |
| AT+CCWA | 呼叫等待通知 | 这个功能默认是关闭的,需要主动开启 |
嗯,这里要注意:HFP 的 AT 指令走的是 RFCOMM 通道,也就是蓝牙串口。车机作为 HF,需要先建立 SLC(Service Level Connection),然后才能收发指令。我曾经在调试时发现,某些手机在连接后不会主动发送 +CIEV 状态通知,导致车机一直以为没有通话。后来我加了个定时轮询 AT+CLCC 才解决。
17.2 CarPhoneService:通话的“大脑”
CarPhoneService 是 Android Automotive 里管理电话功能的核心服务。它封装了蓝牙 HFP 的底层细节,向上层应用提供统一的 API。
它的架构大致是这样的:
CarPhoneService 主要干这几件事:
- 通话状态管理:空闲、拨号、来电、通话中、挂断。每个状态都会广播给所有注册的监听器。
- 号码解析:把从 HFP 收到的号码做格式化,比如去掉国际前缀、补全区号。
- 音频路由:决定通话音频走蓝牙 SCO 通道还是车机内部扬声器。
核心接口:
// 注册通话状态监听
CarPhoneService.registerPhoneStateListener(new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
// state: 0=空闲, 1=来电, 2=通话中
}
});
// 拨号
CarPhoneService.dial("10086");
// 接听/挂断
CarPhoneService.acceptCall();
CarPhoneService.endCall();
我记得有一次,客户反馈说挂断电话后,车机还显示“通话中”。查了半天,发现是 CarPhoneService 没有正确处理 HFP 的 +CLCC 返回的“通话结束”状态。后来我在状态机里加了个超时机制,如果 3 秒内没有收到新的通话状态,就强制切回空闲。
17.3 通话界面开发:从“能用”到“好用”
通话界面,看起来简单,其实细节很多。我把它拆成三个核心场景:
17.3.1 来电界面
来电时,车机必须立即响应。延迟超过 1 秒,用户就会觉得“卡”。
- 全屏显示:来电号码、联系人姓名(如果匹配了通讯录)、头像。
- 接听/拒接按钮:按钮要大,方便驾驶时盲操作。我个人建议最小 64dp。
- 静音选项:有些用户不想接但也不想挂,给个“静音”按钮会贴心很多。
避坑指南:我曾经在来电界面用了 Dialog 形式,结果发现某些车机在低内存时,Dialog 会被系统回收。后来我改用 Activity 并设置 android:turnScreenOn="true" 和 android:showOnLockScreen="true",才彻底解决。
17.3.2 通话中界面
通话中,界面要简洁,但功能不能少。
| 功能 | 实现方式 | 注意点 |
|---|---|---|
| 挂断 | 调用 CarPhoneService.endCall() | 要确认 HFP 的 AT+CHUP 已发送成功 |
| 静音 | 控制麦克风音频通道 | 静音后要给用户明显的视觉反馈 |
| 拨号盘 | 显示 DTMF 按键 | 按下的同时要发送 AT+VTS 指令 |
| 切换音频 | 蓝牙 SCO ↔ 车机扬声器 | 切换时会有短暂断音,要做好缓冲 |
嗯,这里有个细节:通话时长显示。不要直接用 System.currentTimeMillis() 去减,因为通话可能被保持(Hold)。我建议用 CarPhoneService 提供的 getCallStartTime() 接口,它会自动排除保持的时间。
17.3.3 通话记录界面
通话记录,说白了就是从手机同步过来的 Call Log。Android Automotive 通过 HFP 的 CPBR 指令读取手机通讯录,但通话记录一般走的是 PBAP(Phone Book Access Profile)。
我个人习惯把通话记录分为三类:
- 未接来电:红色标记,优先显示。
- 已接来电:绿色标记。
- 已拨电话:蓝色标记。
每条记录要显示:联系人姓名、号码、通话时间、通话时长。如果匹配了通讯录,还可以显示头像。
注意:通话记录的同步不是实时的。有些手机只有在连接 PBAP 时才会同步一次。如果你需要实时更新,可以考虑在每次通话结束后,主动调用 AT+CPBR 重新读取。
17.4 多通话管理:会议电话与呼叫保持
HFP 1.6 及以上版本支持三路通话和会议电话。这在车载场景里很实用,比如你正在通话,又来一个电话,你可以选择保持当前通话、接听新来电,或者合并成会议。
实现多通话的关键是 AT+CHLD 指令:
AT+CHLD=0:释放所有保持的通话AT+CHLD=1:释放当前通话,恢复保持的通话AT+CHLD=2:保持当前通话,接听新来电AT+CHLD=3:合并成会议电话
我在项目中遇到过一个问题:某些手机不支持 CHLD=3,导致会议电话功能失效。后来我加了个能力检测,在连接时发送 AT+BRSF=? 查询手机支持的特性,再决定是否显示“合并通话”按钮。
17.5 音频焦点与 SCO 通道
通话音频走的是蓝牙 SCO(Synchronous Connection-Oriented)通道。这个通道是同步的,专门用于语音,延迟低但带宽也低。
在 Android Automotive 里,音频焦点管理是个大坑。你想想看,车机可能同时在播放音乐、播报导航、还有通话。如果通话来了,音乐必须暂停,导航音量要降低。
我的做法是:
- 通话开始时,通过 AudioManager 请求 AUDIOFOCUS_GAIN_TRANSIENT 焦点。
- 音乐播放器收到焦点丢失通知后,自动暂停。
- 通话结束时,释放焦点,音乐恢复播放。
// 请求音频焦点
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = am.requestAudioFocus(focusListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 打开 SCO 通道
bluetoothAdapter.startVoiceRecognition();
}
嗯,这里要注意:SCO 通道的打开和关闭是有延迟的。我曾经在测试时发现,从请求 SCO 到真正能听到声音,大概有 200-500ms 的延迟。所以不要在用户点击“接听”按钮的瞬间就播放“嘟嘟”声,否则用户会先听到静音,再听到声音,体验很差。
17.6 总结
电话与通讯这块,说白了就是三个层次:底层 HFP 协议、中间层 CarPhoneService、上层通话界面。每一层都有各自的坑,但只要把状态机理清楚、把音频焦点管好、把 AT 指令的兼容性做好,基本就能跑通。
我个人觉得,最难的不是技术实现,而是处理各种手机的兼容性问题。不同品牌的手机,对 HFP 指令的支持程度不一样,返回的数据格式也有差异。我的建议是:在开发阶段就准备一个“手机兼容性测试清单”,把市面上主流的手机都测一遍,提前发现问题。