一、项目全景:全功能手机车机互联系统
说实话,做了这么多年车机互联开发,我一直觉得投屏、控制、电话、媒体、导航这五个功能,就像人的五根手指——分开来每个都能用,但只有握成拳头才有力量。这一章,我们就来搭建这个「拳头」。
这个项目,说白了就是把前面学过的所有知识点串起来。我当年第一次做全功能集成时,踩了不少坑。比如电话模块和媒体模块抢音频焦点,导航投屏时画面撕裂……嗯,这些我都会在课程里一一拆解。
项目核心目标:在 Android Automotive 上实现一个完整的手机车机互联系统,支持投屏、反向控制、蓝牙电话、媒体播放、导航投射五大功能模块。
1.1 系统架构总览
先看整体架构。我习惯把系统分成三层:
- 手机端(Client):负责采集屏幕、音频、传感器数据
- 车机端(Host):负责渲染、交互、本地服务
- 传输层(Transport):WiFi Direct + BLE 混合通道
你想想看,为什么不用纯 WiFi?我在项目中遇到过,纯 WiFi 在车辆进入隧道或地下车库时,连接会瞬间断开。所以加了一条 BLE 通道做心跳保活,效果好了很多。
1.2 五大功能模块详解
每个模块都有自己的脾气。我一个个说。
1.2.1 投屏模块
投屏是整个系统的基础。没有它,其他功能都是空中楼阁。核心思路是:手机端用 MediaCodec 编码屏幕数据,车机端用 SurfaceView 解码渲染。
// 手机端:屏幕采集核心代码
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 4000000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 车机端:解码渲染
Surface surface = surfaceView.getHolder().getSurface();
MediaCodec decoder = MediaCodec.createDecoderByType(MediaFormat.MIME_TYPE_AVC);
decoder.configure(format, surface, null, 0);
decoder.start();
我的经验:码率不要设太高。4Mbps 在大多数场景下够用了。我曾经试过 8Mbps,结果在隧道里卡成幻灯片。另外,I 帧间隔设成 1 秒,这样切屏时不会等太久。
1.2.2 反向控制模块
投屏只是第一步,能控制才是真互联。车机端通过 InputManager 注入触摸事件,手机端需要开启辅助功能服务来接收。
// 车机端注入触摸事件
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
MotionEvent event = MotionEvent.obtain(downTime, eventTime,
MotionEvent.ACTION_DOWN, x, y, 0);
im.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
注意:Android 10 以后,注入事件需要系统级权限。如果是普通应用,可以考虑用 HID 设备模拟触摸。我在项目里就吃过这个亏,后来改用了 USB HID 方案。
1.2.3 电话模块
电话功能走的是蓝牙 HFP 协议。车机端作为 AG(Audio Gateway),手机端作为 HF(Hands-Free)。
| AT 指令 | 功能 | 备注 |
|---|---|---|
| ATD<number>; | 拨打电话 | 分号不能少 |
| ATA | 接听来电 | 振铃时发送 |
| AT+CHUP | 挂断电话 | 通话中或振铃时 |
| AT+VGS= | 调节通话音量 | 0-15 级 |
说白了,AT 指令就是电话模块的「开关」。我刚开始做时,老记不住分号的位置。后来养成了习惯:拨号指令末尾一定加分号,其他指令不加。
1.2.4 媒体模块
媒体播放走 A2DP 协议。车机端注册 MediaBrowserService,手机端通过 MediaSession 控制。
// 车机端:注册媒体浏览器服务
public class CarMediaBrowserService extends MediaBrowserServiceCompat {
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
Bundle rootHints) {
// 只允许特定包名连接
if ("com.android.car".equals(clientPackageName)) {
return new BrowserRoot("root", null);
}
return null;
}
}
避坑指南:我曾经遇到过切歌时卡顿的问题。后来发现是 MediaSession 的回调没处理好。记住:setActive(true) 要在播放前调用,不然系统不认。
1.2.5 导航模块
导航投屏比较特殊。它需要叠加显示在车机地图上,还要处理语音播报。我的做法是:手机端单独开一个导航数据通道,车机端用 Overlay 层显示。
// 导航数据通道:手机端发送导航指令
JSONObject navCmd = new JSONObject();
navCmd.put("action", "NAVIGATE");
navCmd.put("lat", 39.9042);
navCmd.put("lng", 116.4074);
navCmd.put("name", "天安门");
transportChannel.send(navCmd.toString());
1.3 模块间通信与协调
五个模块不是孤立的。它们之间需要协调。比如:
- 电话进来时,媒体要暂停(音频焦点切换)
- 导航播报时,媒体音量要降低(混音策略)
- 投屏画面和导航叠加层要同步(坐标对齐)
我习惯用一个 EventBus 来做模块间通信。每个模块注册自己关心的事件,收到后做相应处理。
// 事件定义
public class AudioFocusEvent {
public static final int TYPE_PHONE = 1;
public static final int TYPE_NAVI = 2;
public static final int TYPE_MEDIA = 3;
public int type;
public boolean isActive;
}
// 媒体模块监听电话事件
EventBus.getDefault().register(this);
@Subscribe
public void onAudioFocus(AudioFocusEvent event) {
if (event.type == AudioFocusEvent.TYPE_PHONE && event.isActive) {
// 暂停播放
mediaPlayer.pause();
}
}
1.4 项目目录结构
最后,我给出这个项目的推荐目录结构。这是我经过多次重构后沉淀下来的,你可以直接拿来用。
CarPhoneProject/
├── app/ # 车机端主应用
│ ├── src/main/java/
│ │ ├── screen/ # 投屏模块
│ │ ├── control/ # 反向控制模块
│ │ ├── phone/ # 电话模块
│ │ ├── media/ # 媒体模块
│ │ ├── navi/ # 导航模块
│ │ └── common/ # 公共工具类
│ └── src/main/res/
├── phoneapp/ # 手机端应用
│ ├── src/main/java/
│ │ ├── capture/ # 屏幕采集
│ │ ├── encode/ # 编码模块
│ │ ├── transport/ # 传输通道
│ │ └── service/ # 后台服务
└── shared/ # 共享协议
└── proto/ # Protobuf 定义
嗯,这一章的内容就到这里。全功能互联系统是个大工程,但拆开来看,每个模块都有章可循。下一章我们开始动手写代码,先从投屏模块的搭建开始。
公众号:蓝海资料掘金营,微信deep3321