Universal Music Player 源碼解析(二)--UI層和playack層的交互

文章集合:
Universal Music Player 源碼解析(一)--MediaSession框架

Univeral Music Player 源碼解析 -- 讓人頭疼的playback

Universal Music Player 源碼解析(二)--MusicService 和 MediaController

Universal Music Player 源碼分析 (三)-- 其他類分析

這篇文章主要承接上文,說明這幾個問題:

  • 如何獲得音樂數(shù)據(jù)?
  • UI 層如何和playback層交互?
  • 令人困惑的MediaId是什么? 他是怎么傳到MediaPlayerActivity中的?

關于音樂數(shù)據(jù)的獲得:

RemoteJSONSource中,發(fā)起了一個http請求,解析成為一個JSONObject

 private JSONObject fetchJSONFromUrl(String urlString)

之后:

private MediaMetadataCompat
 buildFromJSON(JSONObject json, String basePath){
 ...
 String id = String.valueOf(source.hashCode());
....
 return new MediaMetadataCompat.Builder()
                .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
                .putString(MusicProviderSource.CUSTOM_METADATA_TRACK_SOURCE, source)
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
                .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
                .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)
                .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
                .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
                .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)
                .build();
....
}

使用Builder模式,將拿到的數(shù)據(jù),初始化了一個MediaMetaData對象.

這個地方引入了一個新的概念:MediaMetaData是什么?
同樣,還有后面會出現(xiàn)的概念MediaItem ,MediaDescription等 ,我一起總結(jié)一下:

MediaMetaData---音樂元數(shù)據(jù),包含的是很多剛才從RemoteJSONSource中拿到的數(shù)據(jù).
通過MediaMetaData getMediaDescription()可以很容易得到一個MediaDescription對象

MediaItemMediaBrowser的子類

image.png

主要用來表征這個item是browsableorplayable

另外,這里有個很重要的信息: getMediaID() ,同樣具體意義暫且不表,后面會詳細解釋.

很容易可以看出,MediaItem MediaDescription作為MediaBrowser的子類的聯(lián)系很強,同樣getDescription()也可以獲得MediaDescription

所以綜上所述,MediaItem是對音樂數(shù)據(jù)的一個終極封裝,但是詳細的信息,比如:

getMediaUri()

getSubTitle/ title()

只能通過MediaItem.getDescription()再拿一次

音樂數(shù)據(jù)的播放:

剛才的RemoteJSONSource是繼承自MusicProviderSource類的,實現(xiàn)了其中的iterator()

 @Override
    public Iterator<MediaMetadataCompat> iterator() {
    .....
            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));
                    }
                }
            }
      ....
    }

這個方法將會在MusicProvider中被使用:

 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;
            }
        }
    }

于是我們獲得了一個map,value是根據(jù)當時的jsonObject獲取的hashcode,key是一個固定的 string

鋪墊的部分已經(jīng)結(jié)束了,正式來看一下音樂是如何播放的:
如果用戶在界面上點了暫停/播放 這個是怎么實現(xiàn)的呢?

音樂播放的邏輯是結(jié)合MusicService 還有Playback層實現(xiàn)的,具體可以參考一下我這篇博客

如果為了增強閱讀連貫性的讀者可以看看我的總結(jié),迅速上手:

image.png

還有關于handlePlayRequest和MusicService這幾個類的聯(lián)系:


播放的控制是由PlaybackControlsFragment 管理的 ,當點擊播放/暫停的時候:

private final View.OnClickListener mButtonListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            MediaControllerCompat controller = MediaControllerCompat.getMediaController(getActivity());
            PlaybackStateCompat stateObj = controller.getPlaybackState();
            final int state = stateObj == null ?
                    PlaybackStateCompat.STATE_NONE : stateObj.getState();
            LogHelper.d(TAG, "Button pressed, in state " + state);
            switch (v.getId()) {
                case R.id.play_pause:
                    LogHelper.d(TAG, "Play button pressed, in state " + state);
                    if (state == PlaybackStateCompat.STATE_PAUSED ||
                            state == PlaybackStateCompat.STATE_STOPPED ||
                            state == PlaybackStateCompat.STATE_NONE) {
                        playMedia();
                    } else if (state == PlaybackStateCompat.STATE_PLAYING ||
                            state == PlaybackStateCompat.STATE_BUFFERING ||
                            state == PlaybackStateCompat.STATE_CONNECTING) {
                        pauseMedia();
                    }
                    break;
            }
        }
    };

通過conntroller對象獲取到playbackState,調(diào)用playMedia()

 private void playMedia() {
        MediaControllerCompat controller = MediaControllerCompat.getMediaController(getActivity());
        if (controller != null) {
            controller.getTransportControls().play();
        }
    }

出乎意料的簡潔有咩有!
我們知道MediaController是根據(jù)MediaSession.Token創(chuàng)建的,并且綁定在了context上,所以調(diào)用play()就可以控制播放,就會fire MediaSession中的回調(diào),不信請看:

由于在MusicService 中

 mSession.setCallback(mPlaybackManager.getMediaSessionCallback());

看看Callback

private class MediaSessionCallback extends MediaSessionCompat.Callback {
        @Override
        public void onPlay() {
            LogHelper.d(TAG, "play");
            if (mQueueManager.getCurrentMusic() == null) {
                mQueueManager.setRandomQueue();
            }
            handlePlayRequest();
        }
....
}

最終還是調(diào)用handlexxxRequest()

同樣的,跳到下一首:

 @Override
        public void onSkipToQueueItem(long queueId) {
            LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
            mQueueManager.setCurrentQueueItem(queueId);
            mQueueManager.updateMetadata();
        }

最終都會調(diào)用handlePlayRequest()

MediaID的獲取

先看一下MusicService:

   @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid,
                                 Bundle rootHints) {
        LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
                "; clientUid=" + clientUid + " ; rootHints=", rootHints);
        // To ensure you are not allowing any arbitrary app to browse your app's contents, you
        // need to check the origin:
        if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
            // If the request comes from an untrusted package, return an empty browser root.
            // If you return null, then the media browser will not be able to connect and
            // no further calls will be made to other media browsing methods.
            LogHelper.i(TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. "
                    + "Returning empty browser root so all apps can use MediaController."
                    + clientPackageName);
            return new MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null);
        }
        //noinspection StatementWithEmptyBody
 
        return new BrowserRoot(MEDIA_ID_ROOT, null);
    }

    @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()) {
            // if music library is ready, return immediately
            result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
        } else {
            // otherwise, only return results when the music library is retrieved
            result.detach();
            mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
                @Override
                public void onMusicCatalogReady(boolean success) {
                    result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
                }
            });
        }
    }

MusicService 繼承MediaBrowserServiceCompat 并且實現(xiàn)兩個方法:onGetRoot() onLoadChildren()
通過onGetRoot()可以返回一個BrowserRoot實例,需要檢查其他的應用有沒有權(quán)限獲取這個我們的應用中的數(shù)據(jù),
onLoadChildren() 使用了一種特殊的機制返回返回值,這個函數(shù)的返回值是void , 我們通過在MediaBrowseFragment 中subscribe

mMediaFragmentListener.getMediaBrowser().unsubscribe(mMediaId);


   mMediaFragmentListener.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);

看一下mSubscriptionCallback

private final MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback =
            //media description:contains the metadata of a song
            //media item 構(gòu)造函數(shù) (MediaDescription,int flags)
        new MediaBrowserCompat.SubscriptionCallback() {
            @Override
            public void onChildrenLoaded(@NonNull String parentId,
                                         @NonNull List<MediaBrowserCompat.MediaItem> children) {
                try {
                    LogHelper.d(TAG, "fragment onChildrenLoaded, parentId=" + parentId +
                        "  count=" + children.size());
                    checkForUserVisibleErrors(children.isEmpty());
                    mBrowserAdapter.clear();
                    for (MediaBrowserCompat.MediaItem item : children) {
                        mBrowserAdapter.add(item);
                    }
                    mBrowserAdapter.notifyDataSetChanged();
                } catch (Throwable t) {
                    LogHelper.e(TAG, "Error on childrenloaded", t);
                }
            }

            @Override
            public void onError(@NonNull String id) {
                LogHelper.e(TAG, "browse fragment subscription onError, id=" + id);
                Toast.makeText(getActivity(), R.string.error_loading_media, Toast.LENGTH_LONG).show();
                checkForUserVisibleErrors(true);
            }
        };

向adapter中添加內(nèi)容,所以我們才可以瀏覽

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沸移,隨后出現(xiàn)的幾起案子痪伦,更是在濱河造成了極大的恐慌,老刑警劉巖雹锣,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件网沾,死亡現(xiàn)場離奇詭異,居然都是意外死亡蕊爵,警方通過查閱死者的電腦和手機辉哥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來攒射,“玉大人醋旦,你說我怎么就攤上這事』岱牛” “怎么了饲齐?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長咧最。 經(jīng)常有香客問我箩张,道長,這世上最難降的妖魔是什么窗市? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任先慷,我火速辦了婚禮,結(jié)果婚禮上咨察,老公的妹妹穿的比我還像新娘论熙。我一直安慰自己,他們只是感情好摄狱,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布脓诡。 她就那樣靜靜地躺著,像睡著了一般媒役。 火紅的嫁衣襯著肌膚如雪祝谚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天酣衷,我揣著相機與錄音交惯,去河邊找鬼。 笑死,一個胖子當著我的面吹牛席爽,可吹牛的內(nèi)容都是我干的意荤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼只锻,長吁一口氣:“原來是場噩夢啊……” “哼玖像!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起齐饮,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捐寥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后祖驱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體上真,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年羹膳,在試婚紗的時候發(fā)現(xiàn)自己被綠了睡互。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陵像,死狀恐怖就珠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情醒颖,我是刑警寧澤妻怎,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站泞歉,受9級特大地震影響逼侦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腰耙,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一榛丢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧挺庞,春花似錦晰赞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至援制,卻和暖如春戏挡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晨仑。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工褐墅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拆檬,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓掌栅,卻偏偏與公主長得像,于是被迫代替她去往敵國和親码泛。 傳聞我的和親對象是個殘疾皇子猾封,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容