Android车载应用开发与分析(9)- 车载多媒体(四)- 原生音乐播放器(下)

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返回给MediaSourceViewModelMediaSourceViewModel则将其封装在LiveData<BrowsingState>中,供其它有需要的模块监听MediaBrowserService的连接状态。

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<BrowsingState>

/** 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<BrowsingState>,根据不同的连接状态,处理不同的逻辑。

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<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);
    }
}

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<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中实现的播放控制功能。

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<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;
    }

    /**
     * 如果状态中有足够的信息来显示它的 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);
    }
}

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<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的实现,我们等应用都说完后再来一一解释。

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<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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342