第7章 Android Auto通讯应用开发:消息模板、语音输入处理、通话状态管理
通讯应用在车载场景下,说实话,是最刚需的一类。你想想看,开车的时候来消息了,总不能低头看手机吧?Android Auto 给了我们一套完整的模板机制,让消息的展示、回复、语音输入、通话管理都能在安全的前提下完成。这一章,我就带你把这些核心能力一一拆解。
7.1 消息模板:让通知更“车载化”
Android Auto 的消息模板,说白了就是一套专门为驾驶场景设计的 UI 组件。它跟手机上的通知不一样,不能有复杂的布局,也不能有需要精确点击的小按钮。
我个人习惯把消息模板分为三类:
- 简单通知:只显示发件人和消息内容,用户只能“已读”或“忽略”
- 可回复通知:除了显示消息,还提供预设回复或语音输入入口
- 群组通知:多条消息聚合显示,避免刷屏
来看一个最简单的消息模板实现:
// 构建一个可回复的消息通知
val message = MessagingStyle.Message(
"今晚七点老地方见",
System.currentTimeMillis(),
"张三"
)
val style = MessagingStyle(
getPerson("当前用户"),
"张三"
).addMessage(message)
val intent = PendingIntent.getActivity(this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setStyle(style)
.setContentIntent(intent)
.setSmallIcon(R.drawable.ic_message)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.build()
// 别忘了设置 CarAppExtender
val carExtender = CarAppExtender.Builder()
.setImportance(NotificationManagerCompat.IMPORTANCE_HIGH)
.build()
notification.extend(carExtender)
嗯,这里要注意一个坑。我在项目中遇到过,setContentIntent 如果不加 FLAG_IMMUTABLE,在 Android 12 以上会直接崩溃。这个 flag 是 Google 后来强制要求的,很多老代码都没加。
核心要点:消息模板的 MessagingStyle 必须包含至少一条消息,否则 Android Auto 不会显示。另外,Person 对象建议用 Uri 指定头像,这样车载屏幕上能显示联系人头像,体验更好。
7.2 语音输入处理:让用户“动口不动手”
车载场景下,语音输入是王道。Android Auto 提供了两种语音输入方式:一种是系统自带的语音识别,另一种是自定义的 VoiceInteractionService。
我建议大部分开发者直接用系统提供的 RemoteInput,简单可靠:
// 创建语音回复的 RemoteInput
val remoteInput = RemoteInput.Builder("key_voice_reply")
.setLabel("说出你的回复")
.setChoices(arrayOf("好的", "收到", "稍后回复"))
.build()
// 构建回复 Action
val replyAction = NotificationCompat.Action.Builder(
R.drawable.ic_reply,
"回复",
replyPendingIntent
).addRemoteInput(remoteInput)
.build()
// 添加到通知
notificationBuilder.addAction(replyAction)
为什么会这样设计?因为 RemoteInput 会自动唤起车载的语音识别界面,用户说完话后,结果会通过 Intent 回传给你。你不需要自己处理麦克风权限、音频流这些麻烦事。
我曾经踩过一个坑:setChoices 里的预设回复,在 Android Auto 上最多只能显示 3 个,多了会被截断。而且这些预设回复要简短,最好不超过 5 个字,否则屏幕上一行显示不全。
小技巧:语音输入的回传结果,建议做一次简单的文本清洗。比如去掉首尾空格、把“嗯”“啊”这类语气词过滤掉。我在项目里就遇到过用户说“嗯...好的”,结果回复出去变成了“嗯...好的”,看着特别不专业。
7.3 通话状态管理:别让电话打断驾驶
通话状态管理,说白了就是让车载系统知道:现在有没有电话进来?用户在通话中吗?通话结束后要恢复什么状态?
Android Auto 通过 TelecomManager 和 ConnectionService 来管理通话。我一般会维护一个通话状态机,包含以下状态:
| 状态 | 含义 | 触发动作 |
|---|---|---|
| IDLE | 空闲,无通话 | 恢复媒体播放 |
| RINGING | 来电响铃 | 暂停媒体,显示来电界面 |
| ACTIVE | 通话中 | 保持媒体暂停,显示通话时长 |
| HOLD | 通话保持 | 可恢复媒体播放(低音量) |
| DISCONNECTED | 通话结束 | 恢复媒体播放,清理通话界面 |
来看一个通话状态切换的代码片段:
class CallStateManager(private val context: Context) {
private var currentState: CallState = CallState.IDLE
fun onCallStateChanged(state: Int, phoneNumber: String?) {
when (state) {
TelecomManager.EXTRA_STATE_RINGING -> {
currentState = CallState.RINGING
// 暂停媒体播放
pauseMedia()
// 显示来电通知
showIncomingCallNotification(phoneNumber)
}
TelecomManager.EXTRA_STATE_OFFHOOK -> {
currentState = CallState.ACTIVE
// 通话中,保持媒体暂停
keepMediaPaused()
}
TelecomManager.EXTRA_STATE_IDLE -> {
val previousState = currentState
currentState = CallState.IDLE
// 通话结束,恢复媒体
if (previousState == CallState.ACTIVE ||
previousState == CallState.HOLD) {
resumeMedia()
}
// 清理通知
clearCallNotification()
}
}
}
}
你想想看,如果通话结束后没有恢复媒体播放,用户还得手动点一下播放按钮,这在开车时多危险。所以状态管理一定要做完整。
注意:在 Android 10 及以上版本,读取通话状态需要 READ_PHONE_STATE 权限,并且要在运行时动态申请。另外,如果你的应用不是默认的电话应用,TelecomManager 的部分接口可能无法正常工作。我建议用 PhoneStateListener 作为备选方案。
7.4 知识体系总览
这一章的内容比较多,我画了一张图帮你梳理一下整体逻辑:
这张图把三大模块的关系和子内容都串起来了。消息模板负责展示,语音输入负责交互,通话状态管理负责整个生命周期的协调。三者缺一不可。
7.5 实战中的避坑指南
最后,我分享几个实战中遇到的坑,希望能帮你少走弯路:
- 消息通知的优先级:Android Auto 上,
IMPORTANCE_HIGH的消息会弹出横幅通知,IMPORTANCE_DEFAULT只会出现在通知列表里。我建议重要联系人的消息用高优先级,群消息用默认优先级。 - 语音输入的延迟:车载语音识别通常有 1-2 秒的延迟。用户说完话后,不要立即关闭语音界面,给系统一点处理时间。我一般会设置一个 3 秒的超时等待。
- 通话状态的边界情况:比如用户正在通话中,又来了第二个电话(呼叫等待)。这时候状态机会进入 HOLD 状态,原来的通话保持,新通话激活。这个逻辑一定要处理好,否则用户会莫名其妙地断线。
- 测试环境:Android Auto 的通讯功能在模拟器上不太好测。我建议用真机 + 车载模拟器(Desktop Head Unit)来测试,或者直接上车实测。有些问题只有实际跑起来才能发现。
嗯,这一章的内容就到这里。消息模板、语音输入、通话状态管理,这三个能力是车载通讯应用的基石。把这三块吃透了,你的应用在车上跑起来就会很顺畅。