1. LocalMedia
LocalMedia 是 CarAndroid 中自帶的本地音樂播放器席纽,它可以識別出系統(tǒng)中的音樂纽疟,并進行播放瞎嬉。本質(zhì)上屬于一個功能比較完善的Demo蜕琴,官方的目的可能是為了演示如何使用 MediaSession 框架寫一個音樂播放器挣轨。
1.1 LocalMedia 拆解
LocalMedia 運行時分為兩個APP,
-
com.android.car.media.localmediaplayer
該app是一個Service瘫里,主要作用是檢索出本地的音樂多媒體实蔽,并封裝成指定的格式。 -
com.android.car.media
主要用于展示HMI和用戶交互减宣,源碼量非常龐大盐须。
除了上面兩個APP玩荠,其實還有還有一個進程android.car.media漆腌,官方給出的注釋是這么介紹它的:
CarMediaService 管理汽車應(yīng)用程序當前活動的媒體源。 這與 MediaSessionManager 的活動會話不同阶冈,因為汽車中只能有一個活動源闷尿,通過瀏覽和播放。在汽車中女坑,活動媒體源不一定有活動的 MediaSession填具,例如 如果它只是被瀏覽。 但是,該來源仍被視為活動來源劳景,并且應(yīng)該是任何與媒體相關(guān)的 UI(媒體中心誉简、主屏幕等)中顯示的來源。
這里就不介紹CarMediaService盟广,在源碼中被分類com.android.car目錄下闷串,已經(jīng)不是車載應(yīng)用,本質(zhì)上屬于Framework筋量。
之前介紹過com.android.car.media.localmediaplayer 是如何實現(xiàn)的烹吵,接下來介紹com.android.car.media是如何使用com.android.car.media.localmediaplayer
2. HMI 部分源碼分析
LocalMedia的源碼中HMI部分的量尤其的大,而且包含了很多動畫桨武、公共控件肋拔,所以HMI的源碼分析只介紹播放界面,其它部分暫時不做介紹呀酸。
之前解析CarLauncher的源碼時凉蜂,提到過CarLauncher也可以進行Audio的播放,其實就是在寫編譯腳本時七咧,把Media的公共庫一起打包到了CarLauncher中跃惫,這樣就可以在CarLauncher里顯示Audio的播放界面。我們這里就以解析PlaybackFragment的實現(xiàn)流程為主艾栋。
2.1 播放界面源碼結(jié)構(gòu)
播放界面就是一個Fragment爆存,而且也是應(yīng)用開發(fā)中很常見的Fragment+ViewModel+Repository架構(gòu),但是它并沒有完全遵守MVVM架構(gòu)的設(shè)計規(guī)范蝗砾,倒不是因為它沒有使用DataBinding先较,而是因為Fragment的實現(xiàn)中直接調(diào)用了Repository的方法,這不符合MVVM架構(gòu)的設(shè)計思想悼粮。
這里我們先從
MediaSourceViewModel
入手闲勺,開始分析。
2.2 MediaSourceViewModel
MediaSourceViewModel
通過CarMediaManager
來監(jiān)聽當前系統(tǒng)中媒體源扣猫,并使用MediaBrowserConnector
來連接到MediaBrowserService
菜循。
CarMediaManager
是Framework層封裝的API,主要的通信對象是CarMediaService
申尤,關(guān)于CarAndroid中Framework層各個Service的實現(xiàn)癌幕,我們等車載應(yīng)用都說完后再來一一解析。這里我們暫時不需要理解昧穿,因為在實際的車載應(yīng)用開發(fā)中勺远,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
的連接狀態(tài)會通過callback返回給MediaSourceViewModel
胶逢。MediaSourceViewModel
則將其封裝在LiveData<BrowsingState>中,供其它有需要的模塊監(jiān)聽MediaBrowserService的連接狀態(tài)。
2.3 MediaBrowserConnector
MediaBrowserConnector
的邏輯從名字上就能看出來初坠。主要就是創(chuàng)建MediaBrowserCompat
并連接到MediaBrowserService和簸,并把連接過程、連接狀態(tài)以及MediaBrowser的實例封裝在BrowsingState中暴露給MediaSourceViewModel
完成閉環(huán)碟刺。
/**
* 如果給定的 {@link MediaSource} 不為空比搭,則創(chuàng)建并連接一個新的 {@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 可能處于中間狀態(tài)(未連接霉赡,但也未斷開連接。)
// 在這種情況下幔托,再次嘗試連接可以拋出這個異常穴亏,但是不嘗試是無法知道的。
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
使用了單例模式嗓化,在創(chuàng)建過程中會從同樣基于單例模式的MediaSourceViewModel
中獲取到LiveData<BrowsingState>
。
/** One instance per MEDIA_SOURCE_MODE. */
private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2];
/** 返回與給定模式的應(yīng)用程序關(guān)聯(lián)的 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<BrowsingState>刺覆,根據(jù)不同的連接狀態(tài),處理不同的邏輯史煎。
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:
// 清理數(shù)據(jù)
unsubscribeNodes();
clearSearchResults();
clearNodes();
break;
case REJECTED:
case SUSPENDED:
// 連接失敗
onBrowseData(getCache().mRootId, null);
clearSearchResults();
clearNodes();
}
}
如果連接成功谦屑,默認檢索根節(jié)點,并更新本地數(shù)據(jù)篇梭。
2.4.1 基于節(jié)點檢索
/** 返回給定節(jié)點的子數(shù)據(jù)氢橙。 */
public MediaItemsLiveData getMediaChildren(String nodeId) {
PerMediaSourceCache cache = getCache();
MediaChildren items = cache.mChildrenByNodeId.get(nodeId);
if (items == null) {
// 將節(jié)點緩存起來
items = new MediaChildren(nodeId);
cache.mChildrenByNodeId.put(nodeId, items);
}
// 始終刷新訂閱(以解決媒體應(yīng)用程序中的錯誤)。
mBrowsingState.mBrowser.unsubscribe(nodeId);
mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback);
return items.mLiveData;
}
在SubscriptionCallback中更新本地緩存數(shù)據(jù)恬偷,同時也更新對外暴露的MediaItemsLiveData
悍手。
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);
}
};
// 更新節(jié)點的數(shù)據(jù)
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;
}
// 更新緩存中的數(shù)據(jù)
List<MediaItemMetadata> old = children.mPreviousValue;
children.mPreviousValue = list;
// MediaItemsLiveData#onDataLoaded 可以視為帶狀態(tài)的setValue
children.mLiveData.onDataLoaded(old, list);
if (Objects.equals(parentId, cache.mRootId)) {
mRootMediaItems.onDataLoaded(old, list);
}
}
2.4.2 基于關(guān)鍵字檢索
關(guān)鍵字檢索通過search()方法實現(xiàn)。使用時先調(diào)用getSearchMediaItems()
拿到一個LiveData并持續(xù)觀察袍患,再調(diào)用setSearchQuery()
坦康。
/** 設(shè)置搜索查詢。 結(jié)果將通過 {@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);
}
2.5 PlaybackViewModel
MediaBrowserConnector
和MediaItemRepository
分別完成了連接和檢索功能涝焙,接下來就是PlaybackViewModel中實現(xiàn)的播放控制功能卑笨。
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;
}
// 重置舊控制器(如果有)孕暇,在瀏覽未暫停(崩潰)時取消注冊回調(diào)。
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()));
// 應(yīng)用程序并不總是發(fā)送更新,因此請確保我們獲取最新的值妖滔。
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:考慮跟蹤孤立的回調(diào),以防它們復(fù)活......
setMediaController(null);
}
@Override
public void onMetadataChanged(@Nullable MediaMetadataCompat mmdCompat) {
// MediaSession#setMetadata 在其參數(shù)為 null 時構(gòu)建一個空的 MediaMetadata座舍,但 MediaMetadataCompat 不實現(xiàn) equals...
// 因此沮翔,如果給定的 mmdCompat 的 MediaMetadata 等于 EMPTY_MEDIA_METADATA,請將 mMediaMetadata 設(shè)置為 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);
}
}
}
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;
}
/**
* 如果狀態(tài)中有足夠的信息來顯示它的 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();
}
/**
* 返回媒體項的持續(xù)時間(以毫秒為單位)榆鼠。 可以通過調(diào)用 {@link #getProgress()} 獲取此持續(xù)時間內(nèi)的當前位置。
*/
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;
}
/**
* 返回可用自定義操作的排序列表。
* 調(diào)用{@link RawCustomPlaybackAction#fetchDrawable(Context)}以獲得適當?shù)目衫L制圖標伤极。
*/
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);
}
}
3.5.4 封裝媒體控制類
/**
* 為 {@link MediaControllerCompat} 包裝 {@link android.media.session.MediaController.TransportControls TransportControls} 以發(fā)送命令腰鬼。
* TODO(arnaudberry) 這種包裝有意義嗎,因為我們?nèi)匀恍枰獙Πb進行空值檢查塑荒?
* 我們應(yīng)該在模型類上調(diào)用動作方法嗎熄赡?
*/
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);
}
}
/**
* 向媒體源發(fā)送自定義操作
*
* @param action 自定義動作的動作標識符
* @param extras 附加額外數(shù)據(jù)以發(fā)送到媒體源彼硫。
*/
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) {
// 不要將額外內(nèi)容傳回凌箕,因為這不是官方 API拧篮,并且在 media2 中不受支持,因此應(yīng)用程序不應(yīng)依賴于此牵舱。
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<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 發(fā)出 null 時發(fā)出 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』和應(yīng)用的圖標 都是從MediaSourceViewModel
中的getPrimaryMediaSource()
獲取窟蓝。在MediaSourceViewModel
中則是通過CarMediaManager
這個CarAndroid Framework層封裝的API獲取的罪裹,關(guān)于CarAndroid中Framework層的各個Service的實現(xiàn),我們等應(yīng)用都說完后再來一一解釋运挫。
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);
// 媒體列表數(shù)據(jù)
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);
}
以上就是對于HMI部分的分析状共,完整的LocalMedia源碼請見 :https://github.com/linux-link/LocalMedia