博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android 解读开源项目UniversalMusicPlayer(数据管理)
阅读量:5873 次
发布时间:2019-06-19

本文共 12180 字,大约阅读时间需要 40 分钟。

版权声明:本文为博主原创文章,未经博主允许不得转载

源码:
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言

我们主要讲了UAMP项目中播放控制层的实现,而这次就从数据层方面入手,着重分析音频数据服务端展示给用户的过程(ps:UAMP播放器是基于MediaSession框架的,相关资料可参考)

参考资料


项目简介

UAMP播放器作为Google的官方demo展示了如何去开发一款音频媒体应用,该应用可跨多种外接设备使用,并为Android手机,平板电脑,Android Auto,Android Wear,Android TV和Google Cast设备提供一致的用户体验

项目按照标准的MVC架构管理各个模块,模块结构如下图所示

其中modeluiplayback模块分别代表MVC架构中的model层、view层以及controller层。此外,UAMP项目中深度使用了MediaSession框架实现了数据管理、播放控制、UI更新等功能,本系列博客将从各个模块入手,分析其源码及重要功能的实现逻辑,这期主要讲的是数据管理这块的内容


获取音乐库数据

我们在一文中提到,客户端向服务端请求数据的过程从MediaBrowser.subscribe订阅数据开始,到SubscriptionCallback.onChildrenLoaded回调中拿到返回的数据结束,我们就按着这个流程一步步讲解UAMP中音频数据的流向

MediaBrowserFragment是展示音乐列表的界面,在它的onStart方法中发起数据的订阅操作:

public class MediaBrowserFragment extends Fragment {    ...    @Override    public void onAttach(Activity activity) {        super.onAttach(activity);        mMediaFragmentListener = (MediaFragmentListener) activity;    }    @Override    public void onStart() {        ...        MediaBrowserCompat mediaBrowser = mMediaFragmentListener.getMediaBrowser();        if (mediaBrowser.isConnected()) {            onConnected();        }    }        public void onConnected() {        ...        mMediaFragmentListener.getMediaBrowser().unsubscribe(mMediaId);        mMediaFragmentListener.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);    }}复制代码

发起的订阅请求后最终会调用MediaBrowserService.onLoadChildren方法,即请求从客户端来到了Service层:

public class MusicService extends MediaBrowserServiceCompat implements       PlaybackManager.PlaybackServiceCallback {   ...   @Override   public void onLoadChildren(@NonNull final String parentMediaId,                              @NonNull final Result
> result) { LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); if (MEDIA_ID_EMPTY_ROOT.equals(parentMediaId)) {
//如果之前验证客户端没有权限请求数据,则返回一个空的列表 result.sendResult(new ArrayList
()); } else if (mMusicProvider.isInitialized()) {
//如果音乐库已经准备好了,立即返回 result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources())); } else {
//音乐数据检索完毕后返回结果 result.detach(); mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
//加载音乐数据后的回调 @Override public void onMusicCatalogReady(boolean success) { result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources())); } }); } }}复制代码

这里做了两次判断,首先是判断该客户端请求数据的权限是否为空,这个验证的过程在onGetRoot方法中,这个我们后面再细说,总之如果客户端权限为空,Service则会调用result.sendResult方法发送一个空的列表至客户端。第二次判断是Service之前是否已经从服务端获取过一次数据,显然这个判断是为了用户离开MediaBrowserFragment后再次回到这个界面时无需再次与服务端进行交互,直接发送之前的结果即可。当上述两个条件都不符合时,则表示Service需要连接服务端获取数据,这个过程是通过MusicProvider这个类完成的,先来看MusicProvider.retrieveMediaAsync这个方法

//MusicProvider.javapublic void retrieveMediaAsync(final Callback callback) {    LogHelper.d(TAG, "retrieveMediaAsync called");    if (mCurrentState == State.INITIALIZED) {        if (callback != null) {            // Nothing to do, execute callback immediately            callback.onMusicCatalogReady(true);        }        return;    }    new AsyncTask
() { @Override protected State doInBackground(Void... params) { retrieveMedia(); return mCurrentState; } @Override protected void onPostExecute(State current) { if (callback != null) { callback.onMusicCatalogReady(current == State.INITIALIZED); } } }.execute();}public interface Callback { void onMusicCatalogReady(boolean success);}复制代码

这里使用了AsyncTask进行异步获取数据的操作,先来看onPostExecute方法,这里执行了Callback.onMusicCatalogReady回调,由于Callback的实例是在Service层中创建的,即执行回调的结果便是通知Service获取数据完毕,Service可以将数据发送至客户端了。然后再来看doInBackground方法,这里实现了异步获取数据的操作,我们继续跟进retrieveMedia方法:

//MusicProvider.javaprivate synchronized void retrieveMedia() {    try {        if (mCurrentState == State.NON_INITIALIZED) {            mCurrentState = State.INITIALIZING;            Iterator
tracks = mSource.iterator(); while (tracks.hasNext()) { MediaMetadataCompat item = tracks.next(); String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID); mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item)); } buildListsByGenre(); mCurrentState = State.INITIALIZED; } } finally { if (mCurrentState != State.INITIALIZED) { // Something bad happened, so we reset state to NON_INITIALIZED to allow // retries (eg if the network connection is temporary unavailable) mCurrentState = State.NON_INITIALIZED; } }}复制代码

抛开状态位的设置,这个方法可以划分成三个部分来看,其一是拿到mSource的迭代器为接下来的遍历做准备,那么mSource是什么呢?

//MusicProvider.javaprivate MusicProviderSource mSource;复制代码

mSource的类型为MusicProviderSource,这是一个接口,定义了一个常量及一个迭代器:

//MusicProviderSource.javapublic interface MusicProviderSource {    String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";    Iterator
iterator();}复制代码

我们得继续找它的具体实现,这可以在MusicProvider的构造方法中找到:

//MusicProvider.javapublic MusicProvider() {    this(new RemoteJSONSource());}public MusicProvider(MusicProviderSource source) {    mSource = source;    ...}复制代码

那么最终连接服务端并获取数据的操作应该是在RemoteJSONSource这个类完成的,我们重点看下它是如何重写iterator方法的:

//RemoteJSONSource.javapublic class RemoteJSONSource implements MusicProviderSource {    ...    protected static final String CATALOG_URL =        "http://storage.googleapis.com/automotive-media/music.json";    @Override    public Iterator
iterator() { try { int slashPos = CATALOG_URL.lastIndexOf('/'); String path = CATALOG_URL.substring(0, slashPos + 1); JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);//下载JSON文件 ArrayList
tracks = new ArrayList<>(); if (jsonObj != null) { JSONArray jsonTracks = jsonObj.getJSONArray(JSON_MUSIC); if (jsonTracks != null) { for (int j = 0; j < jsonTracks.length(); j++) { tracks.add(buildFromJSON(jsonTracks.getJSONObject(j), path)); } } } return tracks.iterator(); } catch (JSONException e) { LogHelper.e(TAG, e, "Could not retrieve music list"); throw new RuntimeException("Could not retrieve music list", e); } } /** * 解析JSON格式的数据,构建MediaMetadata对象 */ private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath) throws JSONException { ... } /** * 从服务端下载JSON文件,解析并返回JSON object */ private JSONObject fetchJSONFromUrl(String urlString) throws JSONException { ... }}复制代码

代码不复杂,整个流程可以归纳为:根据url从服务端获取封装了音乐源信息的JSON文件解析JSON对象并构建成MediaMetadata对象 → 将所有数据加入列表集合中返回给MusicProvider,至此数据的获取就完成了


构建按类型划分的音频集合

我们回到MusicProvider.retrieveMedia方法。第二步是遍历之前拿到的迭代器数据,取出各个MediaMetadata对象,以键值对的方式重新插入mMusicListById集合中

//MusicProvider.javawhile (tracks.hasNext()) {    MediaMetadataCompat item = tracks.next();    String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);    mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));}复制代码

mMusicListById的类型为ConcurrentHashMap,这点从MusicProvider的构造方法中可以得知,具体资料大家可以自行搜索了解

//MusicProvider.javaprivate final ConcurrentMap
mMusicListById;public MusicProvider(MusicProviderSource source) { ... mMusicListById = new ConcurrentHashMap<>();}复制代码

所有数据保存至mMusicListById集合之后,调用buildListsByGenre方法将这些数据重新按音乐类型进行划分并存至mMusicListByGenre集合中(注意比对Mapvalue类型):

//MusicProvider.javaprivate ConcurrentMap
> mMusicListByGenre;public MusicProvider(MusicProviderSource source) { ... mMusicListByGenre = new ConcurrentHashMap<>();}private synchronized void buildListsByGenre() { ConcurrentMap
> newMusicListByGenre = new ConcurrentHashMap<>(); for (MutableMediaMetadata m : mMusicListById.values()) { String genre = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE); List
list = newMusicListByGenre.get(genre); if (list == null) { list = new ArrayList<>(); newMusicListByGenre.put(genre, list); } list.add(m.metadata); } mMusicListByGenre = newMusicListByGenre;}复制代码

分析一下buildListsByGenre的逻辑:遍历mMusicListById的音频元素,以音频的类型genre作为key值在临时的newMusicListByGenre集合中查找对应的列表,若这个列表为空,则证明之前此类型的音频还未存入newMusicListByGenre中,新建一个空的列表保存当前遍历到的音频元素,并以genre作为key值构建键值对。当遍历到下一个元素时,newMusicListByGenre若已保存了该类型的音频列表,则直接将此元素存进该列表即可。这样通过一次遍历即可将所有音频数据按类型分成多个列表集合,客户端就可以按音频类型选择播放的队列了


更新列表展示数据

buildListsByGenre结束后,设置相应的状态,retrieveMediaAsync中的异步任务,即AsyncTaskdoInBackground的工作就完成了,接下来在onPostExecute中执行回调,回到MusicService中将数据发送至客户端

//MusicService.java@Overridepublic void onLoadChildren(@NonNull final String parentMediaId,                           @NonNull final Result
> result) { ... mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() { //完成音乐加载后的回调 @Override public void onMusicCatalogReady(boolean success) { result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources())); } });}复制代码

客户端(MediaBrowserFragment)拿到数据后刷新列表Adapter即可将内容展示给用户了

//MediaBrowserFragment.javaprivate final MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =               new MediaBrowserCompat.SubscriptionCallback() {    ...    @Override    public void onChildrenLoaded(@NonNull String parentId,                                 @NonNull List
children) { try { ... mBrowserAdapter.clear(); for (MediaBrowserCompat.MediaItem item : children) { mBrowserAdapter.add(item); } mBrowserAdapter.notifyDataSetChanged(); } catch (Throwable t) { LogHelper.e(TAG, "Error on childrenloaded", t); } }};复制代码

MusicProvider其他功能

作为内容提供者,MusicProvider当然不止上述这点功能。MusicProvider支持乱序播放音频,这个主要通过Collections.shuffle方法实现的:

//MusicProvider.javapublic Iterable
getShuffledMusic() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } List
shuffled = new ArrayList<>(mMusicListById.size()); for (MutableMediaMetadata mutableMetadata: mMusicListById.values()) { shuffled.add(mutableMetadata.metadata); } Collections.shuffle(shuffled);//打乱列表的顺序 return shuffled;}复制代码

支持个人“喜欢”,即收藏功能:

//MusicProvider.javaprivate final Set
mFavoriteTracks;public MusicProvider(MusicProviderSource source) { ... mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap
());}public void setFavorite(String musicId, boolean favorite) { if (favorite) { mFavoriteTracks.add(musicId); } else { mFavoriteTracks.remove(musicId); }}/** * 判断该音乐是否在"喜欢"列表中 */public boolean isFavorite(String musicId) { return mFavoriteTracks.contains(musicId);}复制代码

此外还支持多种简易的检索功能:

//MusicProvider.javapublic List
searchMusicBySongTitle(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_TITLE, query);}public List
searchMusicByAlbum(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_ALBUM, query);}public List
searchMusicByArtist(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_ARTIST, query);}public List
searchMusicByGenre(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_GENRE, query);}private List
searchMusic(String metadataField, String query) { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } ArrayList
result = new ArrayList<>(); query = query.toLowerCase(Locale.US); for (MutableMediaMetadata track : mMusicListById.values()) { if (track.metadata.getString(metadataField).toLowerCase(Locale.US) .contains(query)) { result.add(track.metadata); } } return result;}复制代码

那么UAMP播放器数据管理方面的内容到这就暂告一段落了,后续可能会挑UAMP中的一些工具类来讲。最后是惯例:若有什么遗漏或者建议的欢迎留言评论,如果觉得博主写得还不错麻烦点个赞,你们的支持是我最大的动力~

你可能感兴趣的文章
HDU5280 Senior&#39;s Array(简单DP)
查看>>
mysql Access denied for user &#39;root&#39;@&#39;localhost&#39; (using password: YES)
查看>>
VS2015 打开html 提示 未能完成操作 解决办法
查看>>
.NET-"/"应用程序中的服务器错误
查看>>
回击MLAA:NVIDIA FXAA抗锯齿性能实測、画质对照
查看>>
windows tomcat 优化
查看>>
给自定义cell赋值代码
查看>>
GCD
查看>>
linq 实现动态 orderby
查看>>
四版人民币补号大全
查看>>
言未及之而言,谓之躁;言及之而不言,谓之隐;未见颜色而言,谓之瞽(gǔ)...
查看>>
MYSQL查询一周内的数据(最近7天的)
查看>>
Redis的缓存策略和主键失效机制
查看>>
禁止body滚动允许div滚动防微信露底
查看>>
Xtreme8.0 - Kabloom dp
查看>>
jquery css3问卷答题卡翻页动画效果
查看>>
MDK5.00中*** error 65: access violation at 0xFFFFFFFC : no 'write' permission的一种解决方法
查看>>
Android 集成支付宝支付详解
查看>>
SQL分布式查询、跨数据库查询
查看>>
C#------连接SQLServer和MySQL字符串
查看>>