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中,供其它有需要的模块监听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,根据不同的连接状态,处理不同的逻辑。

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

目录
相关文章
|
18天前
|
存储 Android开发 开发者
深入理解安卓应用开发的核心组件
【10月更文挑战第8天】探索Android应用开发的精髓,本文带你了解安卓核心组件的奥秘,包括Activity、Service、BroadcastReceiver和ContentProvider。我们将通过代码示例,揭示这些组件如何协同工作,构建出功能强大且响应迅速的应用程序。无论你是初学者还是资深开发者,这篇文章都将为你提供新的视角和深度知识。
|
2天前
|
传感器 XML IDE
探索安卓应用开发:从基础到进阶
【10月更文挑战第23天】在数字化时代的浪潮中,移动应用已成为人们日常生活的延伸。本文以安卓平台为例,深入浅出地介绍了如何从零开始构建一个安卓应用,涵盖了开发环境搭建、基本组件使用、界面设计原则以及进阶技巧等关键步骤。通过实例演示和代码片段,引导读者逐步掌握安卓应用开发的核心技能,旨在激发更多开发者对安卓平台的探索热情,并为初学者提供一条清晰的学习路径。
|
5天前
|
Java 程序员 开发工具
Android|修复阿里云播放器下载不回调的问题
虽然 GC 带来了很多便利,但在实际编码时,我们也需要注意对象的生命周期管理,该存活的存活,该释放的释放,避免因为 GC 导致的问题。
15 2
|
16天前
|
Java Android开发 Swift
掌握安卓与iOS应用开发:技术比较与选择指南
在移动应用开发领域,谷歌的安卓和苹果的iOS系统无疑是两大巨头。它们不仅塑造了智能手机市场,还影响了开发者的日常决策。本文深入探讨了安卓与iOS平台的技术差异、开发环境及工具、以及市场表现和用户基础。通过对比分析,旨在为开发者提供实用的指导,帮助他们根据项目需求、预算限制和性能要求,做出最合适的平台选择。无论是追求高度定制的用户体验,还是期望快速进入市场,本文都将为您的开发旅程提供有价值的见解。
|
2天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
3天前
|
XML IDE Java
安卓应用开发入门:从零开始的旅程
【10月更文挑战第23天】本文将带领读者开启一段安卓应用开发的奇妙之旅。我们将从最基础的概念讲起,逐步深入到开发实践,最后通过一个简易的代码示例,展示如何将理论知识转化为实际的应用。无论你是编程新手,还是希望扩展技能的软件工程师,这篇文章都将为你提供有价值的指导和启发。
12 0
|
14天前
|
安全 Java 开发工具
掌握安卓应用开发:从基础到高级的全面指南
本文旨在为读者提供一个详尽的指南,帮助他们掌握安卓应用开发的基础知识及高级技巧。从环境搭建到项目实践,逐步深入讲解安卓开发的各个环节。无论是对于刚入门的初学者还是希望进一步提升的开发者,本文都将提供实用的建议和示例代码,帮助你快速上手并提升技能。
|
15天前
|
存储 Java 开发工具
掌握安卓应用开发:从基础到高级的全面指南
这篇文章旨在为读者提供一个关于安卓应用开发的全面指南。无论您是初学者还是有一定经验的开发者,本文将带您深入探讨安卓开发的核心概念、工具和技术。我们将从环境搭建和基本组件讲起,逐步引导您了解布局管理、用户交互处理、数据存储与网络通信等高级主题。通过阅读本文,您将能够更好地理解安卓应用开发的整体流程,并具备创建高质量安卓应用的能力。
|
22天前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。