1. LocalMedia
LocalMedia 是 CarAndroid 中自带的本地音乐播放器,它可以识别出系统中的音乐,并进行播放。本质上属于一个功能比较完善的Demo,官方的目的可能是为了演示如何使用 MediaSession 框架写一个音乐播放器。
1.1 LocalMedia 拆解
LocalMedia 运行时分为两个APP,
- com.android.car.media.localmediaplayer
该app是一个Service,主要作用是检索出本地的音乐多媒体,并封装成指定的格式。 - com.android.car.media
主要用于展示HMI和用户交互,源码量非常庞大。
除了上面两个APP,其实还有还有一个进程android.car.media,官方给出的注释是这么介绍它的:
CarMediaService 管理汽车应用程序当前活动的媒体源。 这与 MediaSessionManager 的活动会话不同,因为汽车中只能有一个活动源,通过浏览和播放。在汽车中,活动媒体源不一定有活动的 MediaSession,例如 如果它只是被浏览。 但是,该来源仍被视为活动来源,并且应该是任何与媒体相关的 UI(媒体中心、主屏幕等)中显示的来源。
这里就不介绍CarMediaService,在源码中被分类com.android.car目录下,已经不是车载应用,本质上属于Framework。
之前介绍过com.android.car.media.localmediaplayer 是如何实现的,接下来介绍com.android.car.media是如何使用com.android.car.media.localmediaplayer
2. HMI 部分源码分析
LocalMedia的源码中HMI部分的量尤其的大,而且包含了很多动画、公共控件,所以HMI的源码分析只介绍播放界面,其它部分暂时不做介绍。
之前解析CarLauncher的源码时,提到过CarLauncher也可以进行Audio的播放,其实就是在写编译脚本时,把Media的公共库一起打包到了CarLauncher中,这样就可以在CarLauncher里显示Audio的播放界面。我们这里就以解析PlaybackFragment的实现流程为主。
2.1 播放界面源码结构
播放界面就是一个Fragment,而且也是应用开发中很常见的Fragment+ViewModel+Repository架构,但是它并没有完全遵守MVVM架构的设计规范,倒不是因为它没有使用DataBinding,而是因为Fragment的实现中直接调用了Repository的方法,这不符合MVVM架构的设计思想。
这里我们先从
MediaSourceViewModel
入手,开始分析。
2.2 MediaSourceViewModel
MediaSourceViewModel
通过CarMediaManager
来监听当前系统中媒体源,并使用MediaBrowserConnector
来连接到MediaBrowserService
。
CarMediaManager
是Framework层封装的API,主要的通信对象是CarMediaService
,关于CarAndroid中Framework层各个Service的实现,我们等车载应用都说完后再来一一解析。这里我们暂时不需要理解,因为在实际的车载应用开发中,CarMediaService
往往都会被裁剪掉。
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返回给MediaSourceViewModel
。MediaSourceViewModel
则将其封装在LiveData
2.3 MediaBrowserConnector
MediaBrowserConnector
的逻辑从名字上就能看出来。主要就是创建MediaBrowserCompat
并连接到MediaBrowserService,并把连接过程、连接状态以及MediaBrowser的实例封装在BrowsingState中暴露给MediaSourceViewModel
完成闭环。
/**
* 如果给定的 {@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);
}
2.4 MediaItemRepository
MediaItemRepository
对外提供媒体项目搜索和子查询功能。
MediaItemRepository
使用了单例模式,在创建过程中会从同样基于单例模式的MediaSourceViewModel
中获取到LiveData
。
/** 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.observeForever(this::onMediaBrowsingStateChanged);
}
通过观察LiveData
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();
}
}
如果连接成功,默认检索根节点,并更新本地数据。
2.4.1 基于节点检索
/** 返回给定节点的子数据。 */
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
。
private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() {
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List children) {
onBrowseData(parentId, children.stream()
.filter(Objects::nonNull)
.map(MediaItemMetadata::new)
.collect(Collectors.toList()));
}
@Override
public void onChildrenLoaded(@NonNull String parentId,
@NonNull List 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 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 old = children.mPreviousValue;
children.mPreviousValue = list;
// MediaItemsLiveData#onDataLoaded 可以视为带状态的setValue
children.mLiveData.onDataLoaded(old, list);
if (Objects.equals(parentId, cache.mRootId)) {
mRootMediaItems.onDataLoaded(old, list);
}
}
2.4.2 基于关键字检索
关键字检索通过search()方法实现。使用时先调用getSearchMediaItems()
拿到一个LiveData并持续观察,再调用setSearchQuery()
。
/** 设置搜索查询。 结果将通过 {@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 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 list) {
mSearchMediaItems.onDataLoaded(null, list);
}
2.5 PlaybackViewModel
MediaBrowserConnector
和MediaItemRepository
分别完成了连接和检索功能,接下来就是PlaybackViewModel中实现的播放控制功能。
3.5.2 封装 MediaControllerCompat.Callback
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 queue) {
List 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);
}
}
}
3.5.3 拓展 PlaybackState
/**
* {@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 getCustomActions() {
List 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);
}
}
3.5.4 封装媒体控制类
/**
* 为 {@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();
}
}
}
2.6 PlaybackFragment
如图所示,播放界面分为显示媒体源信息、显示当前的Audio信息以及播放控制。
2.6.1 显示媒体源信息
private LiveData mMediaSource;
mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource();
// 媒体源 APP名字
mAppName = mapNonNull(mMediaSource, new Function() {
@Override
public CharSequence apply(MediaSource mediaSource) {
return mediaSource.getDisplayName();
}
});
// 媒体源 APP图标
mAppIcon = mapNonNull(mMediaSource, new Function() {
@Override
public Bitmap apply(MediaSource mediaSource) {
return mediaSource.getCroppedPackageIcon();
}
});
/**
* 类似于 Transformations.map(LiveData, Function),但在 source 发出 null 时发出 nullValue。
* func 的输入可能被视为不可为空。
*/
public static LiveData mapNonNull(@NonNull LiveData source,
@NonNull Function func) {
return mapNonNull(source, null, func);
}
public static LiveData mapNonNull(@NonNull LiveData source, @Nullable R nullValue,
@NonNull Function func) {
return Transformations.map(source, new Function() {
@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的实现,我们等应用都说完后再来一一解释。
2.6.2 显示当前播放的媒体信息
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> data) {
if (data.isLoading()) {
mBrowseTreeHasChildren.setValue(null);
return;
}
List items =
MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData());
boolean browseTreeHasChildren = items != null && !items.isEmpty();
mBrowseTreeHasChildren.setValue(browseTreeHasChildren);
}
以上就是对于HMI部分的分析,完整的LocalMedia源码请见 :https://github.com/linux-link/LocalMedia