Android車載應(yīng)用開發(fā)與分析(9)- 車載多媒體(四)- 原生音樂播放器(下)

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

MediaBrowserConnectorMediaItemRepository分別完成了連接和檢索功能涝焙,接下來就是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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市谁帕,隨后出現(xiàn)的幾起案子口芍,更是在濱河造成了極大的恐慌,老刑警劉巖雇卷,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鬓椭,死亡現(xiàn)場離奇詭異,居然都是意外死亡关划,警方通過查閱死者的電腦和手機小染,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贮折,“玉大人裤翩,你說我怎么就攤上這事〉鏖” “怎么了踊赠?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長每庆。 經(jīng)常有香客問我筐带,道長,這世上最難降的妖魔是什么缤灵? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任伦籍,我火速辦了婚禮,結(jié)果婚禮上腮出,老公的妹妹穿的比我還像新娘帖鸦。我一直安慰自己,他們只是感情好胚嘲,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布作儿。 她就那樣靜靜地躺著,像睡著了一般馋劈。 火紅的嫁衣襯著肌膚如雪攻锰。 梳的紋絲不亂的頭發(fā)上晾嘶,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音口注,去河邊找鬼。 笑死君珠,一個胖子當著我的面吹牛寝志,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播策添,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼材部,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唯竹?” 一聲冷哼從身側(cè)響起乐导,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浸颓,沒想到半個月后物臂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡产上,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年棵磷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晋涣。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡仪媒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谢鹊,到底是詐尸還是另有隱情算吩,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布佃扼,位于F島的核電站偎巢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兼耀。R本人自食惡果不足惜艘狭,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望翠订。 院中可真熱鬧巢音,春花似錦、人聲如沸尽超。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽似谁。三九已至傲绣,卻和暖如春掠哥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秃诵。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工续搀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人菠净。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓禁舷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親毅往。 傳聞我的和親對象是個殘疾皇子牵咙,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345