版權聲明:本文爲博主原創文章,未經博主容許不得轉載
源碼: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中的一些工具類來說。最後是慣例:如有什麼遺漏或者建議的歡迎留言評論,若是以爲博主寫得還不錯麻煩點個贊,大家的支持是我最大的動力~