15、车载电话应用:Telephony框架与CarTelephonyService;拨号器应用开发;通话记录与联系人同步;蓝牙电话集成。

各位同学,今天我们来聊聊车载系统里一个非常核心的功能——电话应用。说实话,在车上打电话这个场景,比手机复杂得多。手机你掏出来就拨号,但在车上,你得考虑蓝牙连接、车载麦克风、方向盘控制、甚至还有法规要求(比如行驶中不能手动拨号)。

我当年第一次做车载电话模块时,以为就是调个系统API那么简单。结果呢?连个通话状态都监听不到。后来才发现,Android Automotive的Telephony框架和手机端有挺大区别。今天我就把这些坑和经验,一次性讲清楚。

15.1 Telephony框架与CarTelephonyService

先说说底层架构。Android Automotive的Telephony框架,说白了就是一套管理SIM卡、网络信号、通话状态的系统服务。但车载系统有个特殊点——它不一定有内置的蜂窝模块。很多车是靠蓝牙连接手机来打电话的。

所以Google专门搞了个CarTelephonyService,它是CarService的一部分。这个服务的作用,就是统一管理「车载电话」的各种来源:

  • 内置蜂窝模块(eSIM或物理SIM)
  • 蓝牙免提协议(HFP)
  • 网络电话(VoIP,比如微信语音)

我个人的习惯是,先判断当前车辆有没有内置蜂窝能力。如果有,优先用系统TelephonyManager;如果没有,就全靠蓝牙HFP。判断方法很简单:

TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (tm != null && tm.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE) {
    // 有内置蜂窝模块
} else {
    // 走蓝牙电话
}

嗯,这里要注意:getPhoneType()返回PHONE_TYPE_NONE并不代表不能打电话,只是说没有内置SIM卡。蓝牙电话依然可以工作。

核心概念:CarTelephonyService 封装了 TelephonyManager 和 BluetoothHeadsetClient,对外提供统一的通话控制接口。你开发的拨号器应用,应该通过 CarTelephonyManager 来操作,而不是直接调 TelephonyManager。

15.2 拨号器应用开发

拨号器看起来简单,不就是个数字键盘加一个拨号按钮吗?但车载拨号器有几个特殊要求:

  • 大字体、大按钮——驾驶中操作,不能分散注意力
  • 支持语音输入——方向盘按键触发语音拨号
  • 快速拨号——常用联系人一键拨打
  • 通话中界面——显示通话时长、静音、挂断等

我建议你使用CarTelephonyManager来发起呼叫。它比直接调Intent.ACTION_CALL更可控:

Car car = Car.createCar(context);
CarTelephonyManager carTelephony = (CarTelephonyManager) car.getCarManager(Car.CAR_TELEPHONY_SERVICE);
if (carTelephony != null) {
    carTelephony.placeCall(phoneNumber, Bundle.EMPTY);
}

为什么不用Intent.ACTION_CALL?因为那个会弹出系统拨号器,而车载系统通常需要自定义UI。用CarTelephonyManager.placeCall(),你可以完全控制拨号流程。

我曾经踩过一个坑:在模拟器上测试拨号,结果placeCall()返回成功但没任何反应。后来发现是模拟器没有配置蓝牙HFP服务。所以测试时一定要用真机或带蓝牙模拟的镜像。

15.3 通话记录与联系人同步

通话记录和联系人,这两个数据是电话应用的核心。但车载系统有个特点——数据源可能来自多个地方:

数据源 说明 同步方式
手机联系人 通过蓝牙PBAP协议同步 自动同步到车载数据库
内置SIM卡 存储在SIM卡上的联系人 通过TelephonyManager读取
通话记录 已拨、已接、未接 通过CallLog.Calls内容提供者
云同步 如Google账户联系人 需要账户授权

我个人建议,把联系人数据统一存到一个本地数据库中。每次蓝牙连接时,触发一次全量同步。增量同步太复杂,而且车载场景下用户换手机的概率不高。

读取通话记录的代码很简单:

Cursor cursor = getContentResolver().query(
    CallLog.Calls.CONTENT_URI,
    null, null, null,
    CallLog.Calls.DATE + " DESC"
);
while (cursor.moveToNext()) {
    String number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
    long date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE));
    int type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
    // type: 1=来电, 2=已拨, 3=未接
}

注意:读取通话记录需要READ_CALL_LOG权限。而且从Android 10开始,这个权限属于敏感权限,需要用户主动授予。

权限提醒:不要同时申请READ_CONTACTSREAD_CALL_LOG。用户会觉得很奇怪——一个拨号器为什么要读联系人?最好分步申请,先说明用途。

15.4 蓝牙电话集成

蓝牙电话是车载系统的重头戏。说白了,就是通过蓝牙HFP(Hands-Free Profile)协议,让车机充当手机的免提设备。

Android Automotive里,蓝牙电话的集成主要靠BluetoothHeadsetClient这个类。它负责:

  • 发起和接听电话
  • 控制通话状态(保持、切换、挂断)
  • 获取通话列表(多方通话时)
  • 传输音频(通过SCO连接)

我当年做蓝牙电话时,最头疼的是音频路由。明明已经连接了蓝牙,但通话声音还是从手机扬声器出来。后来发现是SCO连接没建立。SCO是蓝牙的同步面向连接链路,专门传语音的。

建立SCO连接的代码:

BluetoothHeadsetClient headsetClient = ...;
if (headsetClient != null) {
    headsetClient.connectAudio(device);
}

断开时记得调用disconnectAudio(),否则下次通话可能没声音。

还有一个常见问题:蓝牙电话的来电通知。系统会通过TelephonyManager.EXTRA_INCOMING_NUMBER广播来通知。但车载应用通常需要自己监听BluetoothHeadsetClient的回调:

headsetClient.registerCallback(new BluetoothHeadsetClientCallback() {
    @Override
    public void onCallChanged(Call call) {
        // call.getState() 可以获取通话状态
        // Call.STATE_INCOMING, STATE_DIALING, STATE_ACTIVE 等
    }
});

嗯,这里有个细节:onCallChanged回调里的Call对象,不是Android框架里的android.telecom.Call,而是android.bluetooth.BluetoothHeadsetClient.Call。别搞混了。

经验之谈:蓝牙电话的稳定性很大程度上取决于手机端。不同品牌的手机对HFP协议的支持程度不一样。我建议你在测试时,至少覆盖iPhone、三星、小米、华为这四类主流机型。

15.5 知识体系总览

说了这么多,我画了一张图帮你理清整个车载电话应用的知识结构。你看完应该能明白各个模块之间的关系:

车载电话应用知识体系 拨号器应用(UI层) 大按钮键盘 | 通话界面 | 联系人列表 | 通话记录 CarTelephonyService(服务层) 统一管理通话状态、音频路由、多方通话 封装 TelephonyManager + BluetoothHeadsetClient 数据与协议层 蓝牙HFP协议 通话控制 + 音频传输 蓝牙PBAP协议 联系人同步 Telephony框架 SIM卡 + 网络信号 图:车载电话应用三层架构

你看,整个体系分三层:最上面是拨号器UI,中间是CarTelephonyService做统一调度,最下面是蓝牙协议和Telephony框架。每一层只管自己的事,上层不关心底层是蓝牙还是蜂窝。

这种分层设计的好处是——如果以后车机内置了5G模块,你的拨号器代码几乎不用改,只需要在CarTelephonyService里加一个数据源适配就行。

好了,关于车载电话应用的核心内容就这些。说实话,电话功能看起来简单,但涉及蓝牙、权限、多数据源同步,坑确实不少。我建议你动手写一个最小可用的拨号器,从拨号到通话记录显示,走通一遍流程。遇到问题欢迎随时交流。


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