一、项目全景:全功能手机车机互联系统

说实话,做了这么多年车机互联开发,我一直觉得投屏、控制、电话、媒体、导航这五个功能,就像人的五根手指——分开来每个都能用,但只有握成拳头才有力量。这一章,我们就来搭建这个「拳头」。

这个项目,说白了就是把前面学过的所有知识点串起来。我当年第一次做全功能集成时,踩了不少坑。比如电话模块和媒体模块抢音频焦点,导航投屏时画面撕裂……嗯,这些我都会在课程里一一拆解。

项目核心目标:在 Android Automotive 上实现一个完整的手机车机互联系统,支持投屏、反向控制、蓝牙电话、媒体播放、导航投射五大功能模块。

1.1 系统架构总览

先看整体架构。我习惯把系统分成三层:

  • 手机端(Client):负责采集屏幕、音频、传感器数据
  • 车机端(Host):负责渲染、交互、本地服务
  • 传输层(Transport):WiFi Direct + BLE 混合通道

你想想看,为什么不用纯 WiFi?我在项目中遇到过,纯 WiFi 在车辆进入隧道或地下车库时,连接会瞬间断开。所以加了一条 BLE 通道做心跳保活,效果好了很多。

手机端 (Client) 屏幕采集模块 音频编码模块 传感器数据 电话协议栈 导航数据源 媒体播放器 (Android 手机) 传输层 WiFi Direct (视频流) BLE (控制指令) 蓝牙 A2DP/HFP (音频) 车机端 (Host) 投屏渲染引擎 触控事件注入 电话 UI 模块 媒体控制器 导航叠加层 HMI 交互界面 (Android Automotive) 视频流 控制指令

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