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卡。蓝牙电话依然可以工作。
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_CONTACTS和READ_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。别搞混了。
15.5 知识体系总览
说了这么多,我画了一张图帮你理清整个车载电话应用的知识结构。你看完应该能明白各个模块之间的关系:
你看,整个体系分三层:最上面是拨号器UI,中间是CarTelephonyService做统一调度,最下面是蓝牙协议和Telephony框架。每一层只管自己的事,上层不关心底层是蓝牙还是蜂窝。
这种分层设计的好处是——如果以后车机内置了5G模块,你的拨号器代码几乎不用改,只需要在CarTelephonyService里加一个数据源适配就行。
好了,关于车载电话应用的核心内容就这些。说实话,电话功能看起来简单,但涉及蓝牙、权限、多数据源同步,坑确实不少。我建议你动手写一个最小可用的拨号器,从拨号到通话记录显示,走通一遍流程。遇到问题欢迎随时交流。