Android车载多媒体应用LocalMedia

LocalMedia 是 AndroidCar 中自带的本地音乐播放器,它可以识别出系统中的音乐,并进行播放。本质上属于一个功能比较完善的Demo,官方的目的可能是为了演示如何使用 MediaSession 框架写一个音乐播放器。关于MediaSession框架之前已经介绍过了,本篇就简单解析一下这个Demo。

LocalMedia源码组成

源码包主要由三部分构成:

  • com.android.car.media.localmediaplayer
    该app是一个Service,主要作用是检索出本地的音乐多媒体,并封装成元数据。

  • com.android.car.media
    主要用于展示HMI和用户交互,源码量非常庞大。

  • com.android.car.media.common

    lib下的基类库,抽成基类主要是为了方便其他应用引用,比如launcher,因为桌面有播放小组件。

image

其实还有还有一个进程android.car.media,官方给出的注释是这么介绍它的:
CarMediaService 管理汽车应用程序当前活动的媒体源。 这与 MediaSessionManager 的活动会话不同,因为汽车中只能有一个活动源,通过浏览和播放。在汽车中,活动媒体源不一定有活动的 MediaSession,例如 如果它只是被浏览。 但是,该来源仍被视为活动来源,并且应该是任何与媒体相关的 UI(媒体中心、主屏幕等)中显示的来源。

这里就不介绍CarMediaService,在源码中被分类在com.android.car目录下,已经不属于应用的范畴,本质上属于Framework。

我们先来看看com.android.car.media.localmediaplayer 是如何实现。

LocalMediaPlayer源码分析

源码位于:/packages/apps/Car/LocalMediaPlayer

应用的源码分析讨论都是一样的,先从AndroidManifest开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.media.localmediaplayer"
android:sharedUserId="com.android.car.media">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 省略不重要的代码 -->
<application android:theme="@style/LocalMediaPlayerAppTheme">

<service
android:name=".LocalMediaBrowserService"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>

<activity
android:name=".PermissionsActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<meta-data
android:name="distractionOptimized"
android:value="true" />
</activity>

</application>
</manifest>

可以看出Service的结构很简单,LocalMediaBrowserService是MediaSession的容器,PermissionsActivity则是负责权限检查和申请。

LocalMediaBrowserService

LocalMediaBrowserService继承自MediaBrowserService,作为一个容器,主要就是用来初始化其它组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void onCreate() {
super.onCreate();
// 创建 DataModel。
mDataModel = new DataModel(this);
// 初始化 RootItem
addRootItems();
// 创建 MediaSession
mSession = new MediaSession(this, MEDIA_SESSION_TAG);
setSessionToken(mSession.getSessionToken());
// 媒体播放器,同时也是 MediaSession.Callback
mPlayer = new Player(this, mSession, mDataModel);
mSession.setCallback(mPlayer);
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mPlayer.maybeRestoreState();
// 广播,用于监听Notification的控制动作
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_PLAY);
filter.addAction(ACTION_PAUSE);
filter.addAction(ACTION_NEXT);
filter.addAction(ACTION_PREV);
registerReceiver(mNotificationReceiver, filter);
}
  • 创建 DataModel
    用来检索设备本地的多媒体数据。其内部主要封装的都是如何在设备上查询各种多媒体数据。
  • 初始化 RootItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
List<MediaBrowser.MediaItem> mRootItems = new ArrayList<>();

private void addRootItems() {
MediaDescription folders = new MediaDescription.Builder()
.setMediaId(FOLDERS_ID)
.setTitle(getString(R.string.folders_title))
.setIconUri(Utils.getUriForResource(this, R.drawable.ic_folder))
.build();
mRootItems.add(new MediaBrowser.MediaItem(folders, MediaBrowser.MediaItem.FLAG_BROWSABLE));

MediaDescription albums = new MediaDescription.Builder()
.setMediaId(ALBUMS_ID)
.setTitle(getString(R.string.albums_title))
.setIconUri(Utils.getUriForResource(this, R.drawable.ic_album))
.build();
mRootItems.add(new MediaBrowser.MediaItem(albums, MediaBrowser.MediaItem.FLAG_BROWSABLE));

MediaDescription artists = new MediaDescription.Builder()
.setMediaId(ARTISTS_ID)
.setTitle(getString(R.string.artists_title))
.setIconUri(Utils.getUriForResource(this, R.drawable.ic_artist))
.build();
mRootItems.add(new MediaBrowser.MediaItem(artists, MediaBrowser.MediaItem.FLAG_BROWSABLE));

MediaDescription genres = new MediaDescription.Builder()
.setMediaId(GENRES_ID)
.setTitle(getString(R.string.genres_title))
.setIconUri(Utils.getUriForResource(this, R.drawable.ic_genre))
.build();
mRootItems.add(new MediaBrowser.MediaItem(genres, MediaBrowser.MediaItem.FLAG_BROWSABLE));
}

RootItems是在HMI查询ROOT_ID时返回的一个列表,列表中包含四个默认的MediaItem,而且Flag都是***FLAG_BROWSABLE***表示MediaItem是可浏览的(文件夹)。四个MediaItem对应HMI上显示的四个大类。

image

mRootItems会在onLoadChildren()方法中传给HMI端。HMI端需要调用MediaBrowser.subscribe才能触发onLoadChildren()。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onLoadChildren parentId=" + parentId);
}
switch (parentId) {
case ROOT_ID:
result.sendResult(mRootItems);
mLastCategory = parentId;
break;
//.....
}
}
  • 创建 Player
    创建本地播放器。内部主要基于MediaPlayer实现。
  • 添加广播监听
    用来响应Notification中的动作。支持4个动作。

检索/封装Audio - DataModel

DataModel主要用于检索设备本地的多媒体数据,提供了以下四种从ContentProvider检索方式。

文件夹(Folder)检索

1
2
3
4
5
6
7
8
9
private static final Uri[] ALL_AUDIO_URI = new Uri[] {
MediaStore.Audio.Media.INTERNAL_CONTENT_URI,
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
};

public void onQueryByFolder(String parentId, Result<List<MediaItem>> result) {
FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver);
queryInBackground(result, query);
}

专辑(Album)检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final Uri[] ALBUMS_URI = new Uri[] {
MediaStore.Audio.Albums.INTERNAL_CONTENT_URI,
MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
};

public void onQueryByAlbum(String parentId, Result<List<MediaItem>> result) {
QueryTask query = new QueryTask.Builder()
.setResolver(mResolver)
.setResult(result)
.setUri(ALBUMS_URI)
.setKeyColumn(AudioColumns.ALBUM_KEY)
.setTitleColumn(AudioColumns.ALBUM)
.setFlags(MediaItem.FLAG_BROWSABLE)
.build();
queryInBackground(result, query);
}

艺术家(Artist)检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final Uri[] ARTISTS_URI = new Uri[] {
MediaStore.Audio.Artists.INTERNAL_CONTENT_URI,
MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI
};

public void onQueryByArtist(String parentId, Result<List<MediaItem>> result) {
QueryTask query = new QueryTask.Builder()
.setResolver(mResolver)
.setResult(result)
.setUri(ARTISTS_URI)
.setKeyColumn(AudioColumns.ARTIST_KEY)
.setTitleColumn(AudioColumns.ARTIST)
.setFlags(MediaItem.FLAG_BROWSABLE)
.build();
queryInBackground(result, query);
}

流派(Genre)检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final Uri[] GENRES_URI = new Uri[] {
MediaStore.Audio.Genres.INTERNAL_CONTENT_URI,
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI
};

public void onQueryByGenre(String parentId, Result<List<MediaItem>> result) {
QueryTask query = new QueryTask.Builder()
.setResolver(mResolver)
.setResult(result)
.setUri(GENRES_URI)
.setKeyColumn(MediaStore.Audio.Genres._ID)
.setTitleColumn(MediaStore.Audio.Genres.NAME)
.setFlags(MediaItem.FLAG_BROWSABLE)
.build();
queryInBackground(result, query);
}

模糊检索

该方法主要就是检索出设备中所有的Audio数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private static final String QUERY_BY_KEY_WHERE_CLAUSE =
AudioColumns.ALBUM_KEY + "= ? or "
+ AudioColumns.ARTIST_KEY + " = ? or "
+ AudioColumns.TITLE_KEY + " = ? or "
+ AudioColumns.DATA + " like ?";

/**
* 注意:这会清除队列。 在调用此方法之前,应该拥有队列的本地备份。
*/
public void onQueryByKey(String lastCategory, String parentId, Result<List<MediaItem>> result) {
mQueue.clear();
QueryTask.Builder query = new QueryTask.Builder()
.setResolver(mResolver)
.setResult(result);

if (LocalMediaBrowserService.GENRES_ID.equals(lastCategory)) {
// Genre来自不同的表,并且不使用通常媒体表中的 where 子句,因此我们需要有这个条件。
try {
long id = Long.parseLong(parentId);
query.setUri(new Uri[] {
MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id),
MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) });
} catch (NumberFormatException e) {
// 这不应该发生。
Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result");
result.sendResult(new ArrayList<MediaItem>());
return;
}
} else {
query.setUri(ALL_AUDIO_URI)
.setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE)
.setWhereArgs(new String[] { parentId, parentId, parentId, parentId });
}

query.setKeyColumn(AudioColumns.TITLE_KEY)
.setTitleColumn(AudioColumns.TITLE)
.setSubtitleColumn(AudioColumns.ALBUM)
.setFlags(MediaItem.FLAG_PLAYABLE)
.setQueue(mQueue);
queryInBackground(result, query.build());
}

QueryTask

由于ContentProvider#query是一个耗时方法,所以需要放在子线程中执行,于是就有了QueryTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Override
protected Void doInBackground(Void... voids) {
List<MediaItem> results = new ArrayList<>();
long idx = 0;
Cursor cursor = null;
for (Uri uri : mUris) {
try {
cursor = mResolver.query(uri, mColumns, mWhereClause, mWhereArgs, null);
if (cursor != null) {
int keyColumn = cursor.getColumnIndex(mKeyColumn);
int titleColumn = cursor.getColumnIndex(mTitleColumn);
int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
int subtitleColumn = -1;
if (mSubtitleColumn != null) {
subtitleColumn = cursor.getColumnIndex(mSubtitleColumn);
}

while (cursor.moveToNext()) {
Bundle path = new Bundle();
if (pathColumn != -1) {
path.putString(PATH_KEY, cursor.getString(pathColumn));
}

MediaDescription.Builder builder = new MediaDescription.Builder()
.setMediaId(cursor.getString(keyColumn))
.setTitle(cursor.getString(titleColumn))
.setExtras(path);

if (subtitleColumn != -1) {
builder.setSubtitle(cursor.getString(subtitleColumn));
}

MediaDescription description = builder.build();
results.add(new MediaItem(description, mFlags));

// 我们在这里重建队列,所以如果用户选择项目,那么我们可以立即使用这个队列。
if (mQueue != null) {
mQueue.add(new QueueItem(description, idx));
}
idx++;
}
}
} catch (SQLiteException e) {
// 有时,如果媒体扫描尚未看到该类型的数据,则表不存在。
// 例如,在第一次遇到具有流派的歌曲之前,流派表似乎根本不存在。
// 如果我们遇到异常,则永远不会发送结果导致另一端挂断,这是一件坏事。
// 相反,我们可以保持弹性并返回一个空列表。
Log.i(TAG, "Failed to execute query " + e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
mResult.sendResult(results);
return null; // 忽略.
}

QueryTask从名字上就能猜出来,是一个AsyncTask,而且实际只用到了doInBackground(),在后台执行完查询结果后,执行mResult.sendResult(results),结果就会从Service传递给HMI。QueryTask封装了多个可配置参数,还用到一个简单建造者模式,不过我们自己改写的话,用比AsyncTask更轻量的一些的线程池协程即可。

FilesystemListTask

FilesystemListTask 与 QueryTask 一样都是 AsyncTask,FilesystemListTask 主要用于文件夹检索,为了便于理解代码,没有和QueryTask封装在一起。如果强行写在一起,看起来非常奇怪并且过度参数化,有可能变得更加冗长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
protected Void doInBackground(Void... voids) {
Set<String> paths = new HashSet<String>();
Cursor cursor = null;
for (Uri uri : mUris) {
try {
cursor = mResolver.query(uri, COLUMNS, null , null, null);
if (cursor != null) {
int pathColumn = cursor.getColumnIndex(AudioColumns.DATA);
while (cursor.moveToNext()) {
// 我们想要对每首歌曲的路径进行重复数据删除,因此我们只得到一个包含目录的列表。
String fullPath = cursor.getString(pathColumn);
int fileNameStart = fullPath.lastIndexOf(File.separator);
if (fileNameStart < 0) {
continue;
}
String dirPath = fullPath.substring(0, fileNameStart);
paths.add(dirPath);
}
}
} catch (SQLiteException e) {
Log.e(TAG, "Failed to execute query " + e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}

// 取出去重目录列表,并将它们放入结果列表中,以完整目录路径为键,以便我们稍后进行匹配。
List<MediaItem> results = new ArrayList<>();
for (String path : paths) {
int dirNameStart = path.lastIndexOf(File.separator) + 1;
String dirName = path.substring(dirNameStart, path.length());
//在封装为 MediaItem
MediaDescription description = new MediaDescription.Builder()
.setMediaId(path + "%") // 在类似查询中使用。
.setTitle(dirName)
.setSubtitle(path)
.build();
results.add(new MediaItem(description, MediaItem.FLAG_BROWSABLE));
}
mResult.sendResult(results);
return null;
}

媒体播放器-Player

Player继承MediaSession.Callback,所以需要处理HMI端调用MediaController.transportControls.xxx的对应方法。在内部逻辑主要就是记录播放状态和处理音频焦点。

初始化媒体播放器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public Player(Context context, MediaSession session, DataModel dataModel) {
mContext = context;
mDataModel = dataModel;
// 创建AudioManager
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);

mSession = session;
// 创建SharedPreferences用于记录播放状态
mSharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);

mShuffle = new CustomAction.Builder(SHUFFLE, context.getString(R.string.shuffle),
R.drawable.shuffle).build();

mMediaPlayer = new MediaPlayer();
mMediaPlayer.reset();
mMediaPlayer.setOnCompletionListener(mOnCompletionListener);

// 初始化播放器状态,这里设定为error状态
mErrorState = new PlaybackState.Builder()
.setState(PlaybackState.STATE_ERROR, 0, 0)
.setErrorMessage(context.getString(R.string.playback_error))
.build();

// 初始化Notification
mNotificationManager =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 媒体通知有两种形式,播放时需要显示暂停和跳过的控件,暂停时需要显示播放和跳过的控件。
// 预先为这两个设置预先填充的构建器。
Notification.Action prevAction = makeNotificationAction(
LocalMediaBrowserService.ACTION_PREV, R.drawable.ic_prev, R.string.prev);
Notification.Action nextAction = makeNotificationAction(
LocalMediaBrowserService.ACTION_NEXT, R.drawable.ic_next, R.string.next);
Notification.Action playAction = makeNotificationAction(
LocalMediaBrowserService.ACTION_PLAY, R.drawable.ic_play, R.string.play);
Notification.Action pauseAction = makeNotificationAction(
LocalMediaBrowserService.ACTION_PAUSE, R.drawable.ic_pause, R.string.pause);

// 播放时,需要上一个,暂停,下一个。
mPlayingNotificationBuilder = new Notification.Builder(context)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_sd_storage_black)
.addAction(prevAction)
.addAction(pauseAction)
.addAction(nextAction);

// 暂停时,需要上一个,播放,下一个。
mPausedNotificationBuilder = new Notification.Builder(context)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_sd_storage_black)
.addAction(prevAction)
.addAction(playAction)
.addAction(nextAction);
}

// 创建 Notification.Action
private Notification.Action makeNotificationAction(String action, int iconId, int stringId) {
PendingIntent intent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
new Intent(action), PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Action notificationAction = new Notification.Action.Builder(iconId,
mContext.getString(stringId), intent)
.build();
return notificationAction;
}
1
2
3
4
5
6
7
8
9
private OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCompletion()");
}
safeAdvance();
}
};

处理音频焦点

关于如何管理音频焦点可参考官方文档

Android Developer

这里我们看下LocalMedia中的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 申请音频焦点
private boolean requestAudioFocus(Runnable onSuccess) {
int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
onSuccess.run();
return true;
}
Log.e(TAG, "Failed to acquire audio focus");
return false;
}

// 监听音频焦点变化
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focus) {
switch (focus) {
case AudioManager.AUDIOFOCUS_GAIN:
resumePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
pausePlayback();
break;
default:
Log.e(TAG, "Unhandled audio focus type: " + focus);
}
}
};

播放指定的媒体

在HMI端调用 MediaController.transportControls.playFromMediaId()时触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
super.onPlayFromMediaId(mediaId, extras);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras);
}
// 尝试申请音频焦点,申请成功则执行 startPlayback
requestAudioFocus(() -> startPlayback(mediaId));
}

private void startPlayback(String key) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "startPlayback()");
}
List<QueueItem> queue = mDataModel.getQueue();
int idx = 0;
int foundIdx = -1;
for (QueueItem item : queue) {
if (item.getDescription().getMediaId().equals(key)) {
foundIdx = idx;
break;
}
idx++;
}
if (foundIdx == -1) {
mSession.setPlaybackState(mErrorState);
return;
}
mQueue = new ArrayList<>(queue);
mCurrentQueueIdx = foundIdx;
QueueItem current = mQueue.get(mCurrentQueueIdx);
String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY);
MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId());
updateSessionQueueState();
try {
play(path, metadata);
} catch (IOException e) {
Log.e(TAG, "Playback failed.", e);
mSession.setPlaybackState(mErrorState);
}
}

private void play(String path, MediaMetadata metadata) throws IOException {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "play path=" + path + " metadata=" + metadata);
}
mMediaPlayer.reset();
mMediaPlayer.setDataSource(path);
mMediaPlayer.prepare();
if (metadata != null) {
mSession.setMetadata(metadata);
}
// 判断此时是否获取到音频焦点
boolean wasGrantedAudio = requestAudioFocus(() -> {
mMediaPlayer.start();
updatePlaybackStatePlaying();
});
// 没有获取到音频焦点,则暂停播放
if (!wasGrantedAudio) {
pausePlayback();
}
}

getMetadata()DataModel中的方法,主要就是将从ContentProvider中查询到的原始数据,封装成元数据。下面的代码,演示了如何封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public MediaMetadata getMetadata(String key) {
Cursor cursor = null;
MediaMetadata.Builder metadata = new MediaMetadata.Builder();
try {
for (Uri uri : ALL_AUDIO_URI) {
cursor = mResolver.query(uri, null, AudioColumns.TITLE_KEY + " = ?",
new String[]{ key }, null);
if (cursor != null) {
int title = cursor.getColumnIndex(AudioColumns.TITLE);
int artist = cursor.getColumnIndex(AudioColumns.ARTIST);
int album = cursor.getColumnIndex(AudioColumns.ALBUM);
int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID);
int duration = cursor.getColumnIndex(AudioColumns.DURATION);

while (cursor.moveToNext()) {
metadata.putString(MediaMetadata.METADATA_KEY_TITLE,
cursor.getString(title));
metadata.putString(MediaMetadata.METADATA_KEY_ARTIST,
cursor.getString(artist));
metadata.putString(MediaMetadata.METADATA_KEY_ALBUM,
cursor.getString(album));
metadata.putLong(MediaMetadata.METADATA_KEY_DURATION,
cursor.getLong(duration));

String albumArt = null;
Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI,
cursor.getLong(albumId));
try {
InputStream dummy = mResolver.openInputStream(albumArtUri);
albumArt = albumArtUri.toString();
dummy.close();
} catch (IOException e) {
// Ignored because the albumArt is intialized correctly anyway.
}
metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt);
break;
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}

return metadata.build();
}

恢复播放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Override
public void onPlay() {
super.onPlay();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPlay");
}
// 每次尝试播放媒体时都要检查权限
if (!Utils.hasRequiredPermissions(mContext)) {
setMissingPermissionError();
} else {
requestAudioFocus(() -> resumePlayback());
}
}

// 权限检查错误
private void setMissingPermissionError() {
// 启动权限申请用的Activity
Intent prefsIntent = new Intent();
prefsIntent.setClass(mContext, PermissionsActivity.class);
prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, prefsIntent, 0);

// 将播放状态设定未ERROR
Bundle extras = new Bundle();
extras.putString(Utils.ERROR_RESOLUTION_ACTION_LABEL,
mContext.getString(R.string.permission_error_resolve));
extras.putParcelable(Utils.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent);
PlaybackState state = new PlaybackState.Builder()
.setState(PlaybackState.STATE_ERROR, 0, 0)
.setErrorMessage(mContext.getString(R.string.permission_error))
.setExtras(extras)
.build();
mSession.setPlaybackState(state);
}

private void resumePlayback() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "resumePlayback()");
}
// 更新播放状态
updatePlaybackStatePlaying();
if (!mMediaPlayer.isPlaying()) {
mMediaPlayer.start();
}
}

播放时还要同步更新播放状态,并通过MediaSession将状态告知HMI端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 更新播放状态
private void updatePlaybackStatePlaying() {
if (!mSession.isActive()) {
mSession.setActive(true);
}
// 更新媒体会话中的状态。
CustomAction action = new CustomAction
.Builder("android.car.media.localmediaplayer.shuffle",
mContext.getString(R.string.shuffle),
R.drawable.shuffle)
.build();
PlaybackState state = new PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING,
mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED)
.setActions(PLAYING_ACTIONS)
.addCustomAction(action)
.setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
.build();
mSession.setPlaybackState(state);
// 更新媒体样式的通知。
postMediaNotification(mPlayingNotificationBuilder);
}

暂停

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void onPause() {
super.onPause();
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onPause");
}
pausePlayback();
// 放弃音频焦点
mAudioManager.abandonAudioFocus(mAudioFocusListener);
}

private void pausePlayback() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "pausePlayback()");
}
long currentPosition = 0;
if (mMediaPlayer.isPlaying()) {
currentPosition = mMediaPlayer.getCurrentPosition();
mMediaPlayer.pause();
}
// 更新播放状态
PlaybackState state = new PlaybackState.Builder()
.setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED)
.setActions(PAUSED_ACTIONS)
.addCustomAction(mShuffle)
.setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
.build();
mSession.setPlaybackState(state);
// 更新媒体的Notification状态。
postMediaNotification(mPausedNotificationBuilder);
}

终止播放

在Service被销毁时需要终止播放,并销毁播放器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在Service的onDestroy方法中调用
public void destroy() {
stopPlayback();
mNotificationManager.cancelAll();
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mMediaPlayer.release();
}

private void stopPlayback() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "stopPlayback()");
}
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
}
// 更新播放状态
PlaybackState state = new PlaybackState.Builder()
.setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED,
PLAYBACK_SPEED_STOPPED)
.setActions(STOPPED_ACTIONS)
.build();
mSession.setPlaybackState(state);
}

切换下一曲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void onSkipToNext() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onSkipToNext()");
}
safeAdvance();
}

private void safeAdvance() {
try {
advance();
} catch (IOException e) {
Log.e(TAG, "Failed to advance.", e);
mSession.setPlaybackState(mErrorState);
}
}

private void advance() throws IOException {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "advance()");
}
// 如果存在,请转到下一首歌曲。
// 请注意,如果您要支持无缝播放,则必须更改此代码,
// 以便拥有当前正在播放和正在加载的MediaPlayer,并在它们之间进行切换,同时还调用setNextMediaPlayer。
if (mQueue != null && !mQueue.isEmpty()) {
// 当我们跑出当前队列的末尾时,继续循环。
mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size();
playCurrentQueueIndex();
} else {
// 终止播放
stopPlayback();
}
}

切换上一曲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
public void onSkipToPrevious() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onSkipToPrevious()");
}
safeRetreat();
}

private void safeRetreat() {
try {
retreat();
} catch (IOException e) {
Log.e(TAG, "Failed to advance.", e);
mSession.setPlaybackState(mErrorState);
}
}

private void retreat() throws IOException {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "retreat()");
}
// 如果有下一首歌,请转到下一首。请注意,如果要支持无间隙播放,则必须更改此代码,
// 以便在调用setNextMediaPlayer的同时,拥有当前正在播放和正在加载的MediaPlayer,并在两者之间进行切换。
if (mQueue != null) {
// 当我们跑完当前队列的末尾时,继续循环。
mCurrentQueueIdx--;
if (mCurrentQueueIdx < 0) {
mCurrentQueueIdx = mQueue.size() - 1;
}
playCurrentQueueIndex();
} else {
// 终止播放
stopPlayback();
}
}

播放指定的媒体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onSkipToQueueItem(long id) {
try {
mCurrentQueueIdx = (int) id;
playCurrentQueueIndex();
} catch (IOException e) {
Log.e(TAG, "Failed to play.", e);
mSession.setPlaybackState(mErrorState);
}
}

private void playCurrentQueueIndex() throws IOException {
MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription();
String path = next.getExtras().getString(DataModel.PATH_KEY);
MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId());
play(path, metadata);
}

随机播放

随机播放MediaSession.Callback中并没有定义,所以需要使用MediaSession.Callback中提供的onCustomAction进行拓展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Override
public void onCustomAction(String action, Bundle extras) {
switch (action) {
case SHUFFLE:
shuffle();
break;
default:
Log.e(TAG, "Unhandled custom action: " + action);
}
}

/**
* 这是shuffle 的一个简单实现,之前播放的歌曲可能会在shuffle操作后重复。只能从主线程调用此函数。
* shuffle 可以理解为乱序播放。
*/
private void shuffle() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Shuffling");
}
// 以随机的形式重建队列。
if (mQueue != null && mQueue.size() > 2) {
QueueItem current = mQueue.remove(mCurrentQueueIdx);
// 打乱队列顺序
Collections.shuffle(mQueue);
mQueue.add(0, current);
// QueueItem 包含一个队列 id,当用户选择当前播放列表时,该 id 用作键。
// 这意味着必须重建 QueueItems 以设置其新 ID。
for (int i = 0; i < mQueue.size(); i++) {
mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i));
}
mCurrentQueueIdx = 0;
// 更新MediaSession队列状态
updateSessionQueueState();
}
}

private void updateSessionQueueState() {
mSession.setQueueTitle(mContext.getString(R.string.playlist));
mSession.setQueue(mQueue);
}

Media源码分析

该部分为HMI部分,LocalMedia的源码中HMI部分的量尤其的大,而且包含了很多动画、公共控件,所以HMI的源码分析只介绍播放界面,其它部分暂时不做介绍。

image

源码位置:/packages/apps/Car/Media

播放界面源码结构

播放界面就是一个Fragment,而且也是应用开发中很常见的Fragment+ViewModel+Repository架构,但是它并没有完全遵守MVVM架构的设计规范,倒不是因为它没有使用DataBinding,而是因为Fragment的实现中直接调用了Repository的方法,这不符合MVVM架构的设计思想。

img

MediaSourceViewModel

MediaSourceViewModel通过CarMediaManager来监听当前系统中媒体源,并使用MediaBrowserConnector来连接到MediaBrowserService

CarMediaManager是Framework层封装的API,主要的通信对象是CarMediaService,关于CarAndroid中Framework层各个Service的实现,我们等车载应用都说完后再来一一解析。这里我们暂时不需要理解,因为在实际的车载应用开发中,CarMediaService往往都会被裁剪掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void updateModelState(MediaSource newMediaSource) {
MediaSource oldMediaSource = mPrimaryMediaSource.getValue();
if (Objects.equals(oldMediaSource, newMediaSource)) {
return;
}
// 广播新的源
mPrimaryMediaSource.setValue(newMediaSource);

// 从CarMediaManager处拿到媒体源,
if (newMediaSource != null) {
mBrowserConnector.connectTo(newMediaSource);
}
}

private final MediaBrowserConnector mBrowserConnector;

private final MediaBrowserConnector.Callback mBrowserCallback = new MediaBrowserConnector.Callback() {
@Override
public void onBrowserConnectionChanged(@NonNull BrowsingState state) {
mBrowsingState.setValue(state);
}
};

MediaBrowserConnector的连接状态会通过callback返回给MediaSourceViewModelMediaSourceViewModel则将其封装在LiveData中,供其它有需要的模块监听MediaBrowserService的连接状态。

MediaBrowserConnector

MediaBrowserConnector的逻辑从名字上就能看出来。主要就是创建MediaBrowserCompat并连接到MediaBrowserService,并把连接过程、连接状态以及MediaBrowser的实例封装在BrowsingState中暴露给MediaSourceViewModel完成闭环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 如果给定的 {@link MediaSource} 不为空,则创建并连接一个新的 {@link MediaBrowserCompat}。
* 如果需要,之前的浏览器会断开连接。
*
* @param mediaSource 要连接的媒体源。
* @see MediaBrowserCompat#MediaBrowserCompat(Context, ComponentName,
* MediaBrowserCompat.ConnectionCallback, Bundle)
*/
public void connectTo(@Nullable MediaSource mediaSource) {
if (mBrowser != null && mBrowser.isConnected()) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Disconnecting: " + getSourcePackage()
+ " mBrowser: " + idHash(mBrowser));
}
sendNewState(ConnectionStatus.DISCONNECTING);
mBrowser.disconnect();
}

mMediaSource = mediaSource;
if (mMediaSource != null) {
mBrowser = createMediaBrowser(mMediaSource, new BrowserConnectionCallback());
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Connecting to: " + getSourcePackage()
+ " mBrowser: " + idHash(mBrowser));
}
try {
sendNewState(ConnectionStatus.CONNECTING);
mBrowser.connect();
} catch (IllegalStateException ex) {
// 这个comment还有效吗?
// 忽略:MediaBrowse 可能处于中间状态(未连接,但也未断开连接。)
// 在这种情况下,再次尝试连接可以抛出这个异常,但是不尝试是无法知道的。
Log.e(TAG, "Connection exception: " + ex);
sendNewState(ConnectionStatus.SUSPENDED);
}
} else {
mBrowser = null;
}
}

// Override for testing.
@NonNull
protected MediaBrowserCompat createMediaBrowser(@NonNull MediaSource mediaSource,
@NonNull MediaBrowserCompat.ConnectionCallback callback) {
Bundle rootHints = new Bundle();
rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx);
ComponentName browseService = mediaSource.getBrowseServiceComponentName();
return new MediaBrowserCompat(mContext, browseService, callback, rootHints);
}

MediaItemRepository

MediaItemRepository对外提供媒体项目搜索和子查询功能。

MediaItemRepository使用了单例模式,在创建过程中会从同样基于单例模式的MediaSourceViewModel中获取到LiveData<BrowsingState>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** One instance per MEDIA_SOURCE_MODE. */
private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2];

/** 返回与给定模式的应用程序关联的 MediaItemsRepository“单例”。 */
public static MediaItemsRepository get(@NonNull Application application, int mode) {
if (sInstances[mode] == null) {
sInstances[mode] = new MediaItemsRepository(
MediaSourceViewModel.get(application, mode).getBrowsingState()
);
}
return sInstances[mode];
}

@VisibleForTesting
public MediaItemsRepository(LiveData<BrowsingState> browsingState) {
browsingState.observeForever(this::onMediaBrowsingStateChanged);
}

通过观察LiveData,根据不同的连接状态,处理不同的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) {
mBrowsingState = newBrowsingState;
if (mBrowsingState == null) {
Log.e(TAG, "Null browsing state (no media source!)");
return;
}
mBrowsingStateLiveData.setValue(mBrowsingState);
switch (mBrowsingState.mConnectionStatus) {
case CONNECTING:
mRootMediaItems.setLoading();
break;
case CONNECTED:
String rootId = mBrowsingState.mBrowser.getRoot();
getCache().mRootId = rootId;
getMediaChildren(rootId);
break;
case DISCONNECTING:
// 清理数据
unsubscribeNodes();
clearSearchResults();
clearNodes();
break;
case REJECTED:
case SUSPENDED:
// 连接失败
onBrowseData(getCache().mRootId, null);
clearSearchResults();
clearNodes();
}
}

如果连接成功,默认检索根节点,并更新本地数据。

基于节点检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 返回给定节点的子数据。 */
public MediaItemsLiveData getMediaChildren(String nodeId) {
PerMediaSourceCache cache = getCache();
MediaChildren items = cache.mChildrenByNodeId.get(nodeId);
if (items == null) {
// 将节点缓存起来
items = new MediaChildren(nodeId);
cache.mChildrenByNodeId.put(nodeId, items);
}
// 始终刷新订阅(以解决媒体应用程序中的错误)。
mBrowsingState.mBrowser.unsubscribe(nodeId);
mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback);
return items.mLiveData;
}

在SubscriptionCallback中更新本地缓存数据,同时也更新对外暴露的MediaItemsLiveData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() {

@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List<MediaBrowserCompat.MediaItem> children) {
onBrowseData(parentId, children.stream()
.filter(Objects::nonNull)
.map(MediaItemMetadata::new)
.collect(Collectors.toList()));
}

@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List<MediaBrowserCompat.MediaItem> children,
@NonNull Bundle options) {
onChildrenLoaded(parentId, children);
}

@Override
public void onError(@NonNull String parentId) {
onBrowseData(parentId, null);
}

@Override
public void onError(@NonNull String parentId, @NonNull Bundle options) {
onError(parentId);
}
};

// 更新节点的数据
private void onBrowseData(@NonNull String parentId, @Nullable List<MediaItemMetadata> list) {
PerMediaSourceCache cache = getCache();
MediaChildren children = cache.mChildrenByNodeId.get(parentId);
if (children == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Browse parent not in the cache: " + parentId);
}
return;
}
// 更新缓存中的数据
List<MediaItemMetadata> old = children.mPreviousValue;
children.mPreviousValue = list;
// MediaItemsLiveData#onDataLoaded 可以视为带状态的setValue
children.mLiveData.onDataLoaded(old, list);

if (Objects.equals(parentId, cache.mRootId)) {
mRootMediaItems.onDataLoaded(old, list);
}
}

基于关键字检索

关键字检索通过search()方法实现。使用时先调用getSearchMediaItems()拿到一个LiveData并持续观察,再调用setSearchQuery()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/** 设置搜索查询。 结果将通过 {@link #getSearchMediaItems} 给出。 */
public void setSearchQuery(String query) {
mSearchQuery = query;
if (TextUtils.isEmpty(mSearchQuery)) {
clearSearchResults();
} else {
mSearchMediaItems.setLoading();
mBrowsingState.mBrowser.search(mSearchQuery, null, mSearchCallback);
}
}

private final SearchCallback mSearchCallback = new SearchCallback() {
@Override
public void onSearchResult(@NonNull String query, Bundle extras,
@NonNull List<MediaBrowserCompat.MediaItem> items) {
super.onSearchResult(query, extras, items);
if (Objects.equals(mSearchQuery, query)) {
onSearchData(items.stream()
.filter(Objects::nonNull)
.map(MediaItemMetadata::new)
.collect(toList()));
}
}

@Override
public void onError(@NonNull String query, Bundle extras) {
super.onError(query, extras);
if (Objects.equals(mSearchQuery, query)) {
onSearchData(null);
}
}
};

private void onSearchData(@Nullable List<MediaItemMetadata> list) {
mSearchMediaItems.onDataLoaded(null, list);
}

PlaybackViewModel

MediaBrowserConnectorMediaItemRepository分别完成了连接和检索功能,接下来就是PlaybackViewModel中实现的播放控制功能。

封装 MediaControllerCompat.Callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
private class MediaControllerCallback extends MediaControllerCompat.Callback {

private MediaBrowserConnector.BrowsingState mBrowsingState;
private MediaControllerCompat mMediaController;
private MediaMetadataCompat mMediaMetadata;
private PlaybackStateCompat mPlaybackState;

void onMediaBrowsingStateChanged(MediaBrowserConnector.BrowsingState newBrowsingState) {
if (Objects.equals(mBrowsingState, newBrowsingState)) {
Log.w(TAG, "onMediaBrowsingStateChanged noop ");
return;
}

// 重置旧控制器(如果有),在浏览未暂停(崩溃)时取消注册回调。
if (mMediaController != null) {
switch (newBrowsingState.mConnectionStatus) {
case DISCONNECTING:
case REJECTED:
case CONNECTING:
case CONNECTED:
mMediaController.unregisterCallback(this);
// Fall through
case SUSPENDED:
setMediaController(null);
}
}
mBrowsingState = newBrowsingState;
if (mBrowsingState.mConnectionStatus == ConnectionStatus.CONNECTED) {
setMediaController(mInputFactory.getControllerForBrowser(mBrowsingState.mBrowser));
}
}

private void setMediaController(MediaControllerCompat mediaController) {
mMediaMetadata = null;
mPlaybackState = null;
mMediaController = mediaController;
mPlaybackControls.setValue(new PlaybackController(mediaController));

if (mMediaController != null) {
mMediaController.registerCallback(this);
mColors.setValue(mColorsFactory.extractColors(mediaController.getPackageName()));

// 应用程序并不总是发送更新,因此请确保我们获取最新的值。
onMetadataChanged(mMediaController.getMetadata());
onPlaybackStateChanged(mMediaController.getPlaybackState());
onQueueChanged(mMediaController.getQueue());
onQueueTitleChanged(mMediaController.getQueueTitle());
} else {
mColors.setValue(null);
onMetadataChanged(null);
onPlaybackStateChanged(null);
onQueueChanged(null);
onQueueTitleChanged(null);
}

updatePlaybackStatus();
}

@Override
public void onSessionDestroyed() {
Log.w(TAG, "onSessionDestroyed");
// 在MediaSession销毁时unregisterCallback。
//TODO:考虑跟踪孤立的回调,以防它们复活......
setMediaController(null);
}

@Override
public void onMetadataChanged(@Nullable MediaMetadataCompat mmdCompat) {
// MediaSession#setMetadata 在其参数为 null 时构建一个空的 MediaMetadata,但 MediaMetadataCompat 不实现 equals...
// 因此,如果给定的 mmdCompat 的 MediaMetadata 等于 EMPTY_MEDIA_METADATA,请将 mMediaMetadata 设置为 null 以使代码在其他任何地方都更简单。
if ((mmdCompat != null) && EMPTY_MEDIA_METADATA.equals(mmdCompat.getMediaMetadata())) {
mMediaMetadata = null;
} else {
mMediaMetadata = mmdCompat;
}
MediaItemMetadata item =
(mMediaMetadata != null) ? new MediaItemMetadata(mMediaMetadata) : null;
mMetadata.setValue(item);
updatePlaybackStatus();
}

@Override
public void onQueueTitleChanged(CharSequence title) {
mQueueTitle.setValue(title);
}

@Override
public void onQueueChanged(@Nullable List<MediaSessionCompat.QueueItem> queue) {
List<MediaItemMetadata> filtered = queue == null ? Collections.emptyList()
: queue.stream()
.filter(item -> item != null
&& item.getDescription() != null
&& item.getDescription().getTitle() != null)
.map(MediaItemMetadata::new)
.collect(Collectors.toList());
mSanitizedQueue.setValue(filtered);
mHasQueue.setValue(filtered.size() > 1);
}

@Override
public void onPlaybackStateChanged(PlaybackStateCompat playbackState) {
mPlaybackState = playbackState;
updatePlaybackStatus();
}

private void updatePlaybackStatus() {
if (mMediaController != null && mPlaybackState != null) {
mPlaybackStateWrapper.setValue(
new PlaybackStateWrapper(mMediaController, mMediaMetadata, mPlaybackState));
} else {
mPlaybackStateWrapper.setValue(null);
}
}
}

拓展 PlaybackState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
* {@link PlaybackStateCompat} 的扩展。
*/
public static final class PlaybackStateWrapper {

private final MediaControllerCompat mMediaController;
@Nullable
private final MediaMetadataCompat mMetadata;
private final PlaybackStateCompat mState;

PlaybackStateWrapper(@NonNull MediaControllerCompat mediaController,
@Nullable MediaMetadataCompat metadata, @NonNull PlaybackStateCompat state) {
mMediaController = mediaController;
mMetadata = metadata;
mState = state;
}

/**
* 如果状态中有足够的信息来显示它的 UI,则返回 true。
*/
public boolean shouldDisplay() {
// STATE_NONE means no content to play.
return mState.getState() != PlaybackStateCompat.STATE_NONE && ((mMetadata != null) || (
getMainAction() != ACTION_DISABLED));
}

/**
* 返回 主 action
*/
@Action
public int getMainAction() {
@Actions long actions = mState.getActions();
@Action int stopAction = ACTION_DISABLED;
if ((actions & (PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0) {
stopAction = ACTION_PAUSE;
} else if ((actions & PlaybackStateCompat.ACTION_STOP) != 0) {
stopAction = ACTION_STOP;
}

switch (mState.getState()) {
case PlaybackStateCompat.STATE_PLAYING:
case PlaybackStateCompat.STATE_BUFFERING:
case PlaybackStateCompat.STATE_CONNECTING:
case PlaybackStateCompat.STATE_FAST_FORWARDING:
case PlaybackStateCompat.STATE_REWINDING:
case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT:
case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS:
case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM:
return stopAction;
case PlaybackStateCompat.STATE_STOPPED:
case PlaybackStateCompat.STATE_PAUSED:
case PlaybackStateCompat.STATE_NONE:
case PlaybackStateCompat.STATE_ERROR:
return (actions & PlaybackStateCompat.ACTION_PLAY) != 0 ? ACTION_PLAY
: ACTION_DISABLED;
default:
Log.w(TAG, String.format("Unknown PlaybackState: %d", mState.getState()));
return ACTION_DISABLED;
}
}

/**
* 返回当前支持的播放动作
*/
public long getSupportedActions() {
return mState.getActions();
}

/**
* 返回媒体项的持续时间(以毫秒为单位)。 可以通过调用 {@link #getProgress()} 获取此持续时间内的当前位置。
*/
public long getMaxProgress() {
return mMetadata == null ? 0 :
mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
}

/**
* 返回当前媒体源是否正在播放媒体项。
*/
public boolean isPlaying() {
return mState.getState() == PlaybackStateCompat.STATE_PLAYING;
}

/**
* 返回媒体源是否支持跳到下一项。
*/
public boolean isSkipNextEnabled() {
return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0;
}

/**
* 返回媒体源是否支持跳到上一项。
*/
public boolean isSkipPreviousEnabled() {
return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0;
}

/**
* 返回媒体源是否支持在媒体流中寻找新位置。
*/
public boolean isSeekToEnabled() {
return (mState.getActions() & PlaybackStateCompat.ACTION_SEEK_TO) != 0;
}

/**
* 返回媒体源是否需要为跳到下一个操作保留空间。
*/
public boolean isSkipNextReserved() {
return mMediaController.getExtras() != null
&& (mMediaController.getExtras().getBoolean(
MediaConstants.SLOT_RESERVATION_SKIP_TO_NEXT)
|| mMediaController.getExtras().getBoolean(
MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT));
}

/**
* 返回媒体源是否需要为跳到上一个操作保留空间。
*/
public boolean iSkipPreviousReserved() {
return mMediaController.getExtras() != null
&& (mMediaController.getExtras().getBoolean(
MediaConstants.SLOT_RESERVATION_SKIP_TO_PREV)
|| mMediaController.getExtras().getBoolean(
MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV));
}

/**
* 返回媒体源是否正在加载(例如:缓冲、连接等)。
*/
public boolean isLoading() {
int state = mState.getState();
return state == PlaybackStateCompat.STATE_BUFFERING
|| state == PlaybackStateCompat.STATE_CONNECTING
|| state == PlaybackStateCompat.STATE_FAST_FORWARDING
|| state == PlaybackStateCompat.STATE_REWINDING
|| state == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT
|| state == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS
|| state == PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM;
}

/**
* 见 {@link PlaybackStateCompat#getErrorMessage}.
*/
public CharSequence getErrorMessage() {
return mState.getErrorMessage();
}

/**
* 见 {@link PlaybackStateCompat#getErrorCode()}.
*/
public int getErrorCode() {
return mState.getErrorCode();
}

/**
* 见 {@link PlaybackStateCompat#getActiveQueueItemId}.
*/
public long getActiveQueueItemId() {
return mState.getActiveQueueItemId();
}

/**
* 见 {@link PlaybackStateCompat#getState}.
*/
@PlaybackStateCompat.State
public int getState() {
return mState.getState();
}

/**
* 见 {@link PlaybackStateCompat#getExtras}.
*/
public Bundle getExtras() {
return mState.getExtras();
}

@VisibleForTesting
PlaybackStateCompat getStateCompat() {
return mState;
}

/**
* 返回可用自定义操作的排序列表。
* 调用{@link RawCustomPlaybackAction#fetchDrawable(Context)}以获得适当的可绘制图标。
*/
public List<RawCustomPlaybackAction> getCustomActions() {
List<RawCustomPlaybackAction> actions = new ArrayList<>();
RawCustomPlaybackAction ratingAction = getRatingAction();
if (ratingAction != null) actions.add(ratingAction);

for (PlaybackStateCompat.CustomAction action : mState.getCustomActions()) {
String packageName = mMediaController.getPackageName();
actions.add(
new RawCustomPlaybackAction(action.getIcon(), packageName,
action.getAction(),
action.getExtras()));
}
return actions;
}

@Nullable
private RawCustomPlaybackAction getRatingAction() {
long stdActions = mState.getActions();
if ((stdActions & PlaybackStateCompat.ACTION_SET_RATING) == 0) return null;

int ratingType = mMediaController.getRatingType();
if (ratingType != RatingCompat.RATING_HEART) return null;

boolean hasHeart = false;
if (mMetadata != null) {
RatingCompat rating = mMetadata.getRating(
MediaMetadataCompat.METADATA_KEY_USER_RATING);
hasHeart = rating != null && rating.hasHeart();
}

int iconResource = hasHeart ? R.drawable.ic_star_filled : R.drawable.ic_star_empty;
Bundle extras = new Bundle();
extras.putBoolean(EXTRA_SET_HEART, !hasHeart);
return new RawCustomPlaybackAction(iconResource, null, ACTION_SET_RATING, extras);
}
}

封装媒体控制类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 为 {@link MediaControllerCompat} 包装 {@link android.media.session.MediaController.TransportControls TransportControls} 以发送命令。
* TODO(arnaudberry) 这种包装有意义吗,因为我们仍然需要对包装进行空值检查?
* 我们应该在模型类上调用动作方法吗?
*/
public class PlaybackController {
private final MediaControllerCompat mMediaController;

private PlaybackController(@Nullable MediaControllerCompat mediaController) {
mMediaController = mediaController;
}

public void play() {
if (mMediaController != null) {
mMediaController.getTransportControls().play();
}
}

public void skipToPrevious() {
if (mMediaController != null) {
mMediaController.getTransportControls().skipToPrevious();
}
}

public void skipToNext() {
if (mMediaController != null) {
mMediaController.getTransportControls().skipToNext();
}
}

public void pause() {
if (mMediaController != null) {
mMediaController.getTransportControls().pause();
}
}

public void stop() {
if (mMediaController != null) {
mMediaController.getTransportControls().stop();
}
}

/**
* 移动到媒体流中的新位置
*
* @param pos 要移动到的位置,以毫秒为单位。
*/
public void seekTo(long pos) {
if (mMediaController != null) {
PlaybackStateCompat oldState = mMediaController.getPlaybackState();
PlaybackStateCompat newState = new PlaybackStateCompat.Builder(oldState)
.setState(oldState.getState(), pos, oldState.getPlaybackSpeed())
.build();
mMediaControllerCallback.onPlaybackStateChanged(newState);
mMediaController.getTransportControls().seekTo(pos);
}
}

/**
* 向媒体源发送自定义操作
*
* @param action 自定义动作的动作标识符
* @param extras 附加额外数据以发送到媒体源。
*/
public void doCustomAction(String action, Bundle extras) {
if (mMediaController == null) return;
MediaControllerCompat.TransportControls cntrl = mMediaController.getTransportControls();
if (ACTION_SET_RATING.equals(action)) {
boolean setHeart = extras != null && extras.getBoolean(EXTRA_SET_HEART, false);
cntrl.setRating(RatingCompat.newHeartRating(setHeart));
} else {
cntrl.sendCustomAction(action, extras);
}
}

/**
* 开始播放给定的媒体项目。
*/
public void playItem(MediaItemMetadata item) {
if (mMediaController != null) {
// 不要将额外内容传回,因为这不是官方 API,并且在 media2 中不受支持,因此应用程序不应依赖于此。
mMediaController.getTransportControls().playFromMediaId(item.getId(), null);
}
}

/**
* 跳到媒体队列中的特定项目。 此 id 是通过 {@link PlaybackViewModel#getQueue()} 获得的项目的 {@link MediaItemMetadata#mQueueId}。
*/
public void skipToQueueItem(long queueId) {
if (mMediaController != null) {
mMediaController.getTransportControls().skipToQueueItem(queueId);
}
}

public void prepare() {
if (mMediaController != null) {
mMediaController.getTransportControls().prepare();
}
}
}

PlaybackFragment

如图所示,播放界面分为显示媒体源信息、显示当前的Audio信息以及播放控制。

image

显示媒体资源信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private LiveData<MediaSource> mMediaSource;

mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();

// 媒体源 APP名字
mAppName = mapNonNull(mMediaSource, new Function<MediaSource, CharSequence>() {
@Override
public CharSequence apply(MediaSource mediaSource) {
return mediaSource.getDisplayName();
}
});

// 媒体源 APP图标
mAppIcon = mapNonNull(mMediaSource, new Function<MediaSource, Bitmap>() {
@Override
public Bitmap apply(MediaSource mediaSource) {
return mediaSource.getCroppedPackageIcon();
}
});

/**
* 类似于 Transformations.map(LiveData, Function),但在 source 发出 null 时发出 nullValue。
* func 的输入可能被视为不可为空。
*/
public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source,
@NonNull Function<T, R> func) {
return mapNonNull(source, null, func);
}

public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source, @Nullable R nullValue,
@NonNull Function<T, R> func) {
return Transformations.map(source, new Function<T, R>() {
@Override
public R apply(T value) {
if (value == null) {
return nullValue;
} else {
return func.apply(value);
}
}
});
}

从上面的代码可以看出,界面上显示出的『Local Media』和应用的图标 都是从MediaSourceViewModel中的getPrimaryMediaSource()获取。在MediaSourceViewModel中则是通过CarMediaManager这个CarAndroid Framework层封装的API获取的,关于CarAndroid中Framework层的各个Service的实现,我们等应用都说完后再来一一解释。

显示当前播放的媒体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void init(FragmentActivity activity, MediaSourceViewModel mediaSourceViewModel,
PlaybackViewModel playbackViewModel, MediaItemsRepository mediaItemsRepository) {
// 当前播放的媒体的title
mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
// 当前播放的媒体的子title
mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist);
// 媒体列表数据
mMediaItemsRepository.getRootMediaItems()
.observe(activity, this::onRootMediaItemsUpdate);
}

private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) {
if (data.isLoading()) {
mBrowseTreeHasChildren.setValue(null);
return;
}
List<MediaItemMetadata> items =
MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData());

boolean browseTreeHasChildren = items != null && !items.isEmpty();
mBrowseTreeHasChildren.setValue(browseTreeHasChildren);
}