版权声明:本文为博主原创文章,未经博主容许不得转载
源码:AnliaLee/android-UniversalMusicPlayer
你们要是看到有错误的地方或者有啥好的建议,欢迎留言评论java
上篇博客咱们主要讲了UAMP项目中播放控制层的实现,而此次就从数据层方面入手,着重分析音频数据从服务端到展现给用户的过程(ps:UAMP播放器是基于MediaSession框架的,相关资料可参考Android 媒体播放框架MediaSession分析与实践)android
UAMP播放器做为Google的官方demo展现了如何去开发一款音频媒体应用,该应用可跨多种外接设备使用,并为Android手机,平板电脑,Android Auto,Android Wear,Android TV和Google Cast设备提供一致的用户体验github
项目按照标准的MVC架构管理各个模块,模块结构以下图所示json
其中model、ui、playback模块分别表明MVC架构中的model层、view层以及controller层。此外,UAMP项目中深度使用了MediaSession框架实现了数据管理、播放控制、UI更新等功能,本系列博客将从各个模块入手,分析其源码及重要功能的实现逻辑,这期主要讲的是数据管理这块的内容api
咱们在Android 媒体播放框架MediaSession分析与实践一文中提到,客户端向服务端请求数据的过程从MediaBrowser.subscribe订阅数据开始,到SubscriptionCallback.onChildrenLoaded回调中拿到返回的数据结束,咱们就按着这个流程一步步讲解UAMP中音频数据的流向架构
MediaBrowserFragment是展现音乐列表的界面,在它的onStart方法中发起数据的订阅操做:app
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<List<MediaItem>> result) {
LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
if (MEDIA_ID_EMPTY_ROOT.equals(parentMediaId)) {//若是以前验证客户端没有权限请求数据,则返回一个空的列表
result.sendResult(new ArrayList<MediaItem>());
} 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.java
public 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<Void, Void, State>() {
@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.java
private synchronized void retrieveMedia() {
try {
if (mCurrentState == State.NON_INITIALIZED) {
mCurrentState = State.INITIALIZING;
Iterator<MediaMetadataCompat> 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.java
private MusicProviderSource mSource;
复制代码
mSource的类型为MusicProviderSource,这是一个接口,定义了一个常量及一个迭代器:
//MusicProviderSource.java
public interface MusicProviderSource {
String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
Iterator<MediaMetadataCompat> iterator();
}
复制代码
咱们得继续找它的具体实现,这能够在MusicProvider的构造方法中找到:
//MusicProvider.java
public MusicProvider() {
this(new RemoteJSONSource());
}
public MusicProvider(MusicProviderSource source) {
mSource = source;
...
}
复制代码
那么最终链接服务端并获取数据的操做应该是在RemoteJSONSource这个类完成的,咱们重点看下它是如何重写iterator方法的:
//RemoteJSONSource.java
public class RemoteJSONSource implements MusicProviderSource {
...
protected static final String CATALOG_URL =
"http://storage.googleapis.com/automotive-media/music.json";
@Override
public Iterator<MediaMetadataCompat> iterator() {
try {
int slashPos = CATALOG_URL.lastIndexOf('/');
String path = CATALOG_URL.substring(0, slashPos + 1);
JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);//下载JSON文件
ArrayList<MediaMetadataCompat> 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.java
while (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.java
private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
public MusicProvider(MusicProviderSource source) {
...
mMusicListById = new ConcurrentHashMap<>();
}
复制代码
全部数据保存至mMusicListById集合以后,调用buildListsByGenre方法将这些数据从新按音乐类型进行划分并存至mMusicListByGenre集合中(注意比对Map的value类型):
//MusicProvider.java
private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByGenre;
public MusicProvider(MusicProviderSource source) {
...
mMusicListByGenre = new ConcurrentHashMap<>();
}
private synchronized void buildListsByGenre() {
ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByGenre = new ConcurrentHashMap<>();
for (MutableMediaMetadata m : mMusicListById.values()) {
String genre = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE);
List<MediaMetadataCompat> 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中的异步任务,即AsyncTask的doInBackground的工做就完成了,接下来在onPostExecute中执行回调,回到MusicService中将数据发送至客户端
//MusicService.java
@Override
public void onLoadChildren(@NonNull final String parentMediaId, @NonNull final Result<List<MediaItem>> result) {
...
mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
//完成音乐加载后的回调
@Override
public void onMusicCatalogReady(boolean success) {
result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
}
});
}
复制代码
客户端(MediaBrowserFragment)拿到数据后刷新列表Adapter便可将内容展现给用户了
//MediaBrowserFragment.java
private final MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
new MediaBrowserCompat.SubscriptionCallback() {
...
@Override
public void onChildrenLoaded(@NonNull String parentId, @NonNull List<MediaBrowserCompat.MediaItem> 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支持乱序播放音频,这个主要经过Collections.shuffle方法实现的:
//MusicProvider.java
public Iterable<MediaMetadataCompat> getShuffledMusic() {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
List<MediaMetadataCompat> shuffled = new ArrayList<>(mMusicListById.size());
for (MutableMediaMetadata mutableMetadata: mMusicListById.values()) {
shuffled.add(mutableMetadata.metadata);
}
Collections.shuffle(shuffled);//打乱列表的顺序
return shuffled;
}
复制代码
支持我的“喜欢”,即收藏功能:
//MusicProvider.java
private final Set<String> mFavoriteTracks;
public MusicProvider(MusicProviderSource source) {
...
mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
}
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.java
public List<MediaMetadataCompat> searchMusicBySongTitle(String query) {
return searchMusic(MediaMetadataCompat.METADATA_KEY_TITLE, query);
}
public List<MediaMetadataCompat> searchMusicByAlbum(String query) {
return searchMusic(MediaMetadataCompat.METADATA_KEY_ALBUM, query);
}
public List<MediaMetadataCompat> searchMusicByArtist(String query) {
return searchMusic(MediaMetadataCompat.METADATA_KEY_ARTIST, query);
}
public List<MediaMetadataCompat> searchMusicByGenre(String query) {
return searchMusic(MediaMetadataCompat.METADATA_KEY_GENRE, query);
}
private List<MediaMetadataCompat> searchMusic(String metadataField, String query) {
if (mCurrentState != State.INITIALIZED) {
return Collections.emptyList();
}
ArrayList<MediaMetadataCompat> 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中的一些工具类来说。最后是惯例:如有什么遗漏或者建议的欢迎留言评论,若是以为博主写得还不错麻烦点个赞,大家的支持是我最大的动力~