5. Android Auto媒体应用开发:音频焦点管理、播放列表构建、通知与锁屏控制
好,咱们今天聊点硬核的。Android Auto上的媒体应用,说白了就是让用户在开车时能安全地听歌、听播客。但这里头坑不少。我最早做车载媒体开发时,以为就是把手机端的播放器搬过来,结果被音频焦点和锁屏控制折腾得够呛。今天我把这些经验掰开揉碎,跟你讲清楚。
5.1 音频焦点管理:别让音乐打架
音频焦点是什么?简单说,就是系统用来协调多个App谁出声的机制。你想想看,导航在说“前方300米右转”,这时候音乐还在大声放,司机能听清吗?
Android Auto里,音频焦点管理比手机端更严格。因为车载场景下,导航、电话、语音助手都是高优先级。我见过不少App,焦点处理不当,导致导航播报时音乐忽大忽小,用户体验极差。
5.1.1 请求与释放焦点
核心API就两个:requestAudioFocus() 和 abandonAudioFocus()。但怎么用,有讲究。
// 请求音频焦点
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChangeListener)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 焦点获取成功,开始播放
startPlayback();
} else if (result == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
// 焦点被延迟授予,比如电话正在响铃
// 这时候不要播放,等待回调
waitForFocus();
}
这里有个细节:setAcceptsDelayedFocusGain(true)。我在项目中遇到过,如果不设置这个,电话挂断后你的App可能永远拿不到焦点。系统会认为你放弃了。
5.1.2 焦点变化监听
焦点不是一次性的。你播放过程中,导航可能突然插进来。这时候需要监听焦点变化:
private AudioManager.OnAudioFocusChangeListener focusChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS:
// 永久失去焦点,比如另一个媒体App开始播放
// 应该停止播放
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时失去焦点,比如导航播报
// 暂停播放,等恢复
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可以降低音量继续播放
// 比如导航播报时,音乐降低音量
setVolume(0.3f);
break;
case AudioManager.AUDIOFOCUS_GAIN:
// 重新获得焦点
// 恢复播放或恢复音量
resumePlayback();
break;
}
}
};
注意:千万别在 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 时直接静音。我见过有些App直接 mute,结果导航播报结束后音乐没了,用户以为App挂了。降低音量到30%左右是比较合理的做法。
5.2 播放列表构建:MediaSession与MediaBrowserService
Android Auto的媒体应用,核心是 MediaSession 和 MediaBrowserService。这两个东西,说白了就是你的播放器和车载系统之间的桥梁。
5.2.1 MediaSession:播放控制中心
MediaSession负责接收来自车载屏幕的播放指令:播放、暂停、下一首、上一首。它就像个遥控器接收器。
MediaSessionCompat mediaSession = new MediaSessionCompat(context, "MyMediaSession");
mediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
// 用户点击了播放按钮
startPlayback();
}
@Override
public void onPause() {
// 用户点击了暂停
pausePlayback();
}
@Override
public void onSkipToNext() {
// 下一首
playNextTrack();
}
@Override
public void onSkipToPrevious() {
// 上一首
playPreviousTrack();
}
@Override
public void onSeekTo(long pos) {
// 拖动进度条
seekToPosition(pos);
}
});
// 别忘了设置可用的播放动作
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
嗯,这里有个坑。我刚开始做的时候,忘了设置 FLAG_HANDLES_TRANSPORT_CONTROLS,结果车载屏幕上播放按钮一直是灰色的。折腾了半天才发现是少了个flag。
5.2.2 MediaBrowserService:内容提供者
MediaBrowserService负责向车载系统提供你的播放列表。系统通过它来浏览你的音乐库、播客列表。
public class MyMediaBrowserService extends MediaBrowserServiceCompat {
@Override
public void onCreate() {
super.onCreate();
// 初始化你的媒体会话
mediaSession = new MediaSessionCompat(this, "MyMediaBrowserService");
setSessionToken(mediaSession.getSessionToken());
}
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName,
int clientUid,
@Nullable Bundle rootHints) {
// 验证客户端身份,Android Auto系统会通过
if (clientPackageName.equals("com.google.android.projection.gearhead")) {
return new BrowserRoot("root", null);
}
return null; // 拒绝非车载系统的访问
}
@Override
public void onLoadChildren(@NonNull String parentId,
@NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
// 返回子节点列表
List<MediaBrowserCompat.MediaItem> items = new ArrayList<>();
if (parentId.equals("root")) {
// 根节点:返回分类
items.add(createCategoryItem("最近播放", "recent"));
items.add(createCategoryItem("我的歌单", "playlists"));
items.add(createCategoryItem("播客订阅", "podcasts"));
} else if (parentId.equals("playlists")) {
// 返回歌单列表
items.add(createMediaItem("摇滚精选", "playlist_1"));
items.add(createMediaItem("轻音乐", "playlist_2"));
}
result.sendResult(items);
}
private MediaBrowserCompat.MediaItem createCategoryItem(String title, String id) {
MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.setSubtitle("浏览")
.build();
return new MediaBrowserCompat.MediaItem(description,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
}
private MediaBrowserCompat.MediaItem createMediaItem(String title, String id) {
MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.build();
return new MediaBrowserCompat.MediaItem(description,
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
}
}
小技巧:如果你的播放列表很大,比如有几千首歌,别一次性全返回。用分页加载,每次返回20-50条。我在项目中遇到过,一次性返回上千条,车载系统直接卡死。
5.3 通知与锁屏控制
车载场景下,用户不能操作手机。所以通知和锁屏控制是唯一的交互入口。这部分做不好,用户只能干瞪眼。
5.3.1 媒体通知:让用户控制播放
Android Auto要求媒体应用必须发布一个媒体通知。这个通知会显示在车载屏幕上,包含播放/暂停、上一首/下一首按钮。
// 构建媒体通知
MediaSessionCompat.Token token = mediaSession.getSessionToken();
MediaStyle mediaStyle = new MediaStyle()
.setMediaSession(token)
.setShowActionsInCompactView(0, 1, 2) // 在紧凑模式下显示三个按钮
.setShowCancelButton(true);
// 创建播放动作
NotificationCompat.Action playAction = new NotificationCompat.Action(
R.drawable.ic_play, "播放",
MediaButtonReceiver.buildMediaButtonPendingIntent(this,
PlaybackStateCompat.ACTION_PLAY));
NotificationCompat.Action pauseAction = new NotificationCompat.Action(
R.drawable.ic_pause, "暂停",
MediaButtonReceiver.buildMediaButtonPendingIntent(this,
PlaybackStateCompat.ACTION_PAUSE));
NotificationCompat.Action nextAction = new NotificationCompat.Action(
R.drawable.ic_skip_next, "下一首",
MediaButtonReceiver.buildMediaButtonPendingIntent(this,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT));
// 构建通知
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music_note)
.setContentTitle("当前播放:摇滚精选")
.setContentText("歌手:某乐队")
.setLargeIcon(albumArt)
.setStyle(mediaStyle)
.addAction(playAction) // 索引0
.addAction(pauseAction) // 索引1
.addAction(nextAction) // 索引2
.setOngoing(true)
.build();
// 启动前台服务
startForeground(NOTIFICATION_ID, notification);
注意,setShowActionsInCompactView(0, 1, 2) 这个参数。它决定了在通知栏折叠状态下显示哪几个按钮。我一般把播放/暂停放在中间,上一首和下一首放两边。
5.3.2 锁屏控制:Android Auto的专属界面
Android Auto的锁屏控制,其实是通过MediaSession自动实现的。你只需要正确设置播放状态:
// 更新播放状态
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_SKIP_TO_NEXT |
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
PlaybackStateCompat.ACTION_SEEK_TO |
PlaybackStateCompat.ACTION_STOP)
.setState(PlaybackStateCompat.STATE_PLAYING, // 或 STATE_PAUSED
currentPosition, // 当前进度
1.0f) // 播放速度
.build();
mediaSession.setPlaybackState(state);
// 更新元数据
MediaMetadataCompat metadata = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, "摇滚精选")
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "某乐队")
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "专辑名")
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt)
.build();
mediaSession.setMetadata(metadata);
核心要点:Android Auto的锁屏界面完全由系统根据MediaSession的播放状态和元数据自动渲染。你不需要自己画界面。但前提是——你必须正确设置 PlaybackState 和 MediaMetadata。少一个字段,界面就可能显示不全。
5.4 知识体系总览
下面这张图,把今天讲的内容串起来了。你可以看到音频焦点、MediaSession、MediaBrowserService、通知这四个模块如何协同工作。
5.5 避坑指南
最后,分享几个我踩过的坑:
- 音频焦点不释放:我曾经有个bug,App退出后音频焦点没释放,导致导航一直没声音。后来在
onDestroy()里加了abandonAudioFocus()才解决。 - MediaSession Token 未设置:MediaBrowserService 里忘了调用
setSessionToken(),结果车载系统一直连不上。这个错很低级,但很容易忘。 - 通知渠道兼容性:Android 8.0以上必须创建通知渠道。我见过有些App在低版本手机上崩溃,就是因为没做版本判断。
- 前台服务权限:Android 9以上需要
FOREGROUND_SERVICE权限。别忘了在Manifest里声明。
我的建议:开发时先用模拟器调试。Android Auto模拟器虽然慢,但能模拟大部分车载场景。真机调试时,记得把手机连上车机,或者用 Desktop Head Unit 工具。别像我一样,在模拟器上跑得好好的,一上车就翻车。
好了,这一章的内容就这些。音频焦点、播放列表、通知锁屏,这三个东西是Android Auto媒体应用的基石。把它们搞扎实了,后面的开发就顺了。