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

目录
相关文章
|
11天前
|
Android开发 Swift iOS开发
深入探索iOS与Android操作系统的架构差异及其对应用开发的影响
在当今数字化时代,移动设备已经成为我们日常生活和工作不可或缺的一部分。其中,iOS和Android作为全球最流行的两大移动操作系统,各自拥有独特的系统架构和设计理念。本文将深入探讨iOS与Android的系统架构差异,并分析这些差异如何影响应用开发者的开发策略和用户体验设计。通过对两者的比较,我们可以更好地理解它们各自的优势和局限性,从而为开发者提供有价值的见解,帮助他们在这两个平台上开发出更高效、更符合用户需求的应用。
|
20天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
20天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
20天前
|
Android开发 Swift iOS开发
探索iOS与安卓应用开发的差异性
探索iOS与安卓应用开发的差异性
41 2
|
15天前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
26天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
Android开发 Windows
Android实现背景音乐播放
Android实现背景音乐播放
324 0
Android实现背景音乐播放
|
6天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
11天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
13天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
下一篇
无影云桌面