4. Android Auto项目创建:配置AndroidManifest.xml、实现MediaBrowserService、处理媒体会话

好,咱们进入实战环节。这一章我会带你亲手搭建一个Android Auto项目。说白了,就是让车机系统能识别你的App,并且能正常播放音乐。

我个人习惯先把项目结构理清楚,再动手写代码。你想想看,车载开发最怕什么?最怕车机连上了,但媒体服务没启动,或者会话没处理好。嗯,这些坑我都踩过。

4.1 项目基础配置

先创建一个普通的Android项目,包名建议用你公司的域名倒序。比如 com.example.caraudio。然后我们要做两件事:

  • build.gradle 里引入Android Auto依赖
  • AndroidManifest.xml 里声明媒体服务

依赖这块,我用的是 androidx.media:media:1.6.0 以上版本。注意别用旧版的 android.support,那玩意儿在车载上兼容性很差。我在项目中遇到过因为依赖版本不对,导致车机无法识别App的情况,折腾了两天才发现是版本问题。

// build.gradle (Module: app)
dependencies {
    implementation 'androidx.media:media:1.6.0'
    implementation 'androidx.car.app:app:1.3.0'
    // 其他依赖...
}

接下来是清单文件。这里有个关键点:MediaBrowserService 必须声明在 <application> 标签内,并且要加上 android:exported="true"。为什么?因为车机系统需要通过跨进程通信来绑定你的服务。

<!-- AndroidManifest.xml -->
<application ...>
    <service
        android:name=".MyMediaBrowserService"
        android:exported="true"
        android:foregroundServiceType="mediaPlayback">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService" />
        </intent-filter>
    </service>

    <!-- 声明你的App支持Android Auto -->
    <meta-data
        android:name="com.google.android.gms.car.application"
        android:resource="@xml/automotive_app_desc" />
</application>
注意: foregroundServiceType 在Android 12以上是必填的。我曾经漏掉这个属性,结果App在车机上直接崩溃,日志提示"Foreground service type not set"。别问我怎么知道的。

还需要创建一个 res/xml/automotive_app_desc.xml 文件,告诉车机你的App支持哪些功能:

<!-- res/xml/automotive_app_desc.xml -->
<automotiveApp>
    <uses name="media" />
</automotiveApp>

4.2 实现MediaBrowserService

这是整个车载媒体App的核心。MediaBrowserService 负责向车机提供媒体内容树。车机会通过 onGetRoot()onLoadChildren() 这两个方法来获取你的音乐列表。

我画了一张图,帮你理解整个数据流向:

MediaBrowserService 数据流向图 车机系统 Android Auto / CarPlay onGetRoot() MediaBrowserService onLoadChildren() 返回MediaItem 媒体数据源 本地/网络音乐 详细流程 1. 车机启动 → 绑定 MediaBrowserService 2. 调用 onGetRoot() → 返回根节点ID 3. 调用 onLoadChildren(根节点ID) → 返回专辑/歌单列表 4. 用户选择 → 继续 onLoadChildren() 或播放 5. 播放时 → 更新 MediaSession 状态

来看代码实现。首先继承 MediaBrowserServiceCompat

public class MyMediaBrowserService extends MediaBrowserServiceCompat {
    private static final String TAG = "MyMediaBrowserService";
    private MediaSessionCompat mediaSession;
    private PlayerAdapter playerAdapter;

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化媒体会话
        mediaSession = new MediaSessionCompat(this, TAG);
        mediaSession.setFlags(
            MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
            MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
        );
        setSessionToken(mediaSession.getSessionToken());
        
        // 初始化播放器
        playerAdapter = new PlayerAdapter(this, mediaSession);
        
        // 设置回调
        mediaSession.setCallback(new MediaSessionCallback(playerAdapter));
    }

    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName,
                                  int clientUid,
                                  @Nullable Bundle rootHints) {
        // 验证客户端包名,防止被恶意App调用
        if (!getPackageName().equals(clientPackageName)) {
            // 可以在这里做更严格的包名校验
            return null;
        }
        return new BrowserRoot("__ROOT__", null);
    }

    @Override
    public void onLoadChildren(@NonNull String parentId,
                                @NonNull Result<List<MediaItem>> result) {
        result.sendResult(MediaDataRepository.getMediaItems(parentId));
    }
}
我的经验: onGetRoot() 里的包名校验一定要做。我之前有个项目没做校验,结果第三方App也能绑定我的服务,导致车机上出现了不该出现的音乐列表。安全无小事。

4.3 处理媒体会话

MediaSession 是车机和App之间的通信桥梁。车机通过它来控制播放、暂停、切歌等操作。说白了,MediaSession 就是遥控器,车机按按钮,App执行动作。

我们需要实现一个 MediaSessionCompat.Callback

private class MediaSessionCallback extends MediaSessionCompat.Callback {
    private final PlayerAdapter playerAdapter;

    MediaSessionCallback(PlayerAdapter playerAdapter) {
        this.playerAdapter = playerAdapter;
    }

    @Override
    public void onPlay() {
        playerAdapter.play();
    }

    @Override
    public void onPause() {
        playerAdapter.pause();
    }

    @Override
    public void onSkipToNext() {
        playerAdapter.skipToNext();
    }

    @Override
    public void onSkipToPrevious() {
        playerAdapter.skipToPrevious();
    }

    @Override
    public void onPlayFromMediaId(String mediaId, Bundle extras) {
        playerAdapter.playFromMediaId(mediaId);
    }

    @Override
    public void onSeekTo(long pos) {
        playerAdapter.seekTo(pos);
    }
}

然后,在 PlayerAdapter 中,每次播放状态变化时,都要更新 MediaSession 的元数据:

public class PlayerAdapter {
    private MediaSessionCompat mediaSession;
    private MediaMetadataCompat currentMetadata;
    private int state = PlaybackStateCompat.STATE_NONE;

    public void playFromMediaId(String mediaId) {
        // 加载音乐信息
        MediaData data = MediaDataRepository.getMedia(mediaId);
        
        // 更新元数据
        currentMetadata = new MediaMetadataCompat.Builder()
            .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
            .putString(MediaMetadataCompat.METADATA_KEY_TITLE, data.title)
            .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, data.artist)
            .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, data.album)
            .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, data.duration)
            .build();
        
        mediaSession.setMetadata(currentMetadata);
        
        // 更新播放状态
        state = PlaybackStateCompat.STATE_PLAYING;
        PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
            .setActions(getAvailableActions())
            .setState(state, 0, 1.0f)
            .build();
        mediaSession.setPlaybackState(playbackState);
        
        // 开始播放...
    }

    private long getAvailableActions() {
        long actions = PlaybackStateCompat.ACTION_PLAY |
                       PlaybackStateCompat.ACTION_PAUSE |
                       PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
                       PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
                       PlaybackStateCompat.ACTION_SEEK_TO;
        return actions;
    }
}
关键点: 车机显示的音乐信息(歌名、歌手、专辑封面)全部来自 MediaMetadataCompat。如果你不设置元数据,车机上就只会显示"未知曲目"。我见过不少新手犯这个错。

4.4 避坑指南

最后,我总结几个实战中容易踩的坑:

  • 服务未启动: 车机连接后,MediaBrowserService 必须已经运行。建议在 Application 的 onCreate() 中提前启动服务。
  • 会话Token不一致: setSessionToken() 必须在 onCreate() 中调用,且只能调用一次。重复调用会导致车机无法绑定。
  • 包名校验过严: 有些车机系统(比如某些第三方车机)的包名可能不在你的白名单里。建议至少放行 com.google.android.gms 和系统UI的包名。
  • 前台服务通知: Android 13以上,媒体播放必须显示前台服务通知。别忘了在 startForeground() 中传入通知。

我曾经在一个项目中,因为忘记在 onGetRoot() 中返回正确的根节点,导致车机一直显示"正在加载"...排查了整整一个下午。你想想看,车机连上了,但就是不出音乐列表,那种感觉真的很崩溃。

好了,这一章的内容就到这里。配置清单、实现服务、处理会话,这三步走完,你的App就已经能在车机上被识别了。下一章我们会深入媒体数据的加载和展示,让车机真正"唱起来"。


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