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 架构图 通话界面 App 联系人 App 通话记录 App CarPhoneService 通话状态管理 · 号码解析 · 音频路由 HFP 适配层(AT 指令解析 + 事件分发) 蓝牙协议栈(RFCOMM / HFP)

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 里,音频焦点管理是个大坑。你想想看,车机可能同时在播放音乐、播报导航、还有通话。如果通话来了,音乐必须暂停,导航音量要降低。

我的做法是:

  1. 通话开始时,通过 AudioManager 请求 AUDIOFOCUS_GAIN_TRANSIENT 焦点。
  2. 音乐播放器收到焦点丢失通知后,自动暂停。
  3. 通话结束时,释放焦点,音乐恢复播放。
// 请求音频焦点
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 指令的支持程度不一样,返回的数据格式也有差异。我的建议是:在开发阶段就准备一个“手机兼容性测试清单”,把市面上主流的手机都测一遍,提前发现问题。