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

目录
相关文章
|
1月前
|
搜索推荐 Android开发 iOS开发
安卓与iOS系统的用户界面设计对比分析
本文通过对安卓和iOS两大操作系统的用户界面设计进行对比分析,探讨它们在设计理念、交互方式、视觉风格等方面的差异及各自特点,旨在帮助读者更好地理解和评估不同系统的用户体验。
23 1
|
2月前
|
Android开发 数据安全/隐私保护 iOS开发
安卓与iOS系统的发展趋势与比较分析
【2月更文挑战第6天】 在移动互联网时代,安卓和iOS系统作为两大主流移动操作系统,各自呈现出不同的发展趋势。本文将从技术角度出发,对安卓和iOS系统的发展方向、特点及未来趋势进行比较分析,以期为读者提供更深入的了解和思考。
38 4
|
5天前
|
存储 Java Android开发
Android系统 设置第三方应用为默认Launcher实现和原理分析
Android系统 设置第三方应用为默认Launcher实现和原理分析
18 0
|
5天前
|
存储 Java Linux
Android系统获取event事件回调等几种实现和原理分析
Android系统获取event事件回调等几种实现和原理分析
26 0
|
2月前
|
存储 数据库 Android开发
|
7天前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
24 1
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
29天前
|
Java Android开发
Android 开发获取通知栏权限时会出现两个应用图标
Android 开发获取通知栏权限时会出现两个应用图标
14 0
|
3天前
|
数据库 Android开发 开发者
安卓应用开发:构建高效用户界面的策略
【4月更文挑战第24天】 在竞争激烈的移动应用市场中,一个流畅且响应迅速的用户界面(UI)是吸引和保留用户的关键。针对安卓平台,开发者面临着多样化的设备和系统版本,这增加了构建高效UI的复杂性。本文将深入分析安卓平台上构建高效用户界面的最佳实践,包括布局优化、资源管理和绘制性能的考量,旨在为开发者提供实用的技术指南,帮助他们创建更流畅的用户体验。
|
20天前
|
XML 开发工具 Android开发
构建高效的安卓应用:使用Jetpack Compose优化UI开发
【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。