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() 这两个方法来获取你的音乐列表。
我画了一张图,帮你理解整个数据流向:
来看代码实现。首先继承 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