AnliaLee/android-UniversalMusicPlayer if you see anything wrong or have a good suggestion, please leave a comment

preface

In the previous blog, we mainly talked about the implementation of the playback control layer in the UAMP project, and this time we will start from the data layer, focusing on the analysis of the audio data from the server to the process of showing to the user (PS: UAMP player is based on MediaSession framework, relevant information can refer to Android media playback framework MediaSession analysis and practice)

Resources googlesamples/android-UniversalMusicPlayer


Project introduction

UAMP Player is an official Google demo that shows how to develop an audio media application that can be used across a variety of external devices and for Android phones, tablets, Android Auto, Android Wear, Android TV and Google Cast devices provide a consistent user experience

The project manages each module according to the standard MVC architecture, and the module structure is shown in the figure below

Model, UI, and Playback modules respectively represent the Model layer, View layer, and Controller layer in THE MVC architecture. In addition, MediaSession framework is deeply used in the UAMP project to realize data management, play control, UI update and other functions. This series of blogs will start from each module to analyze the source code and the implementation logic of important functions. This period mainly talks about the content of data management


Get music library data

Android media Playback framework MediaSession analyzes and Practices the process of a client requesting data from a server starts with mediabrowser.subscribe. To SubscriptionCallback. OnChildrenLoaded callback to get the data returned by the end, we explain UAMP step by step according to the process of the flow of audio data

MediaBrowserFragment is the interface that displays the music list and initiates the subscription of the data in its onStart method:

public class MediaBrowserFragment extends Fragment {...@Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mMediaFragmentListener = (MediaFragmentListener) activity;
    }

    @Override
    public void onStart(a) {... MediaBrowserCompat mediaBrowser = mMediaFragmentListener.getMediaBrowser();if(mediaBrowser.isConnected()) { onConnected(); }}public void onConnected(a) {... mMediaFragmentListener.getMediaBrowser().unsubscribe(mMediaId); mMediaFragmentListener.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback); }}Copy the code

A subscription request will eventually call MediaBrowserService. OnLoadChildren method, namely the request from the client to the Service layer:

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)) {// An empty list is returned if the previously validated client did not have permission to request data
           result.sendResult(new ArrayList<MediaItem>());
       } else if (mMusicProvider.isInitialized()) {// If the music library is ready, return immediately
           result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources()));
       } else {// Return the result after the music data is retrieved
           result.detach();
           mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {// Callback after loading music data
               @Override
               public void onMusicCatalogReady(boolean success) { result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources())); }}); }}}Copy the code

The onGetRoot method checks whether the client’s permission to request data is null. This is done in the onGetRoot method, which will be discussed later. If the client’s permission is null, the Service will call result.sendResult to send an empty list to the client. The second check is whether the Service has received data from the server before. When the user returns to the interface after leaving the MediaBrowserFragment, the user does not need to interact with the server again and can directly send the previous result. When the above two conditions are in accordance, said the Service needs to connect the server to get data, the process is done by MusicProvider this class, first to see MusicProvider. RetrieveMediaAsync this method

//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);
}
Copy the code

Here use AsyncTask for asynchronous data operation, take the onPostExecute method, here to perform the Callback. OnMusicCatalogReady Callback, due to the Callback instance is created in the Service layer, The result of the callback is to notify the Service that the data has been retrieved and that the Service can send the data to the client. Then look at the doInBackground method, which implements the asynchronous retrieval of data, and follow up with the retrieveMedia method:

//MusicProvider.java
private synchronized void retrieveMedia(a) {
    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, newMutableMediaMetadata(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; }}}Copy the code

Regardless of the setting of the state bits, this method can be divided into three parts. One is to get the iterator of the mSource to prepare for the following traversal. So what is the mSource?

//MusicProvider.java
private MusicProviderSource mSource;
Copy the code

MSource is of type MusicProviderSource, which is an interface that defines a constant and an iterator:

//MusicProviderSource.java
public interface MusicProviderSource {
    String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
    Iterator<MediaMetadataCompat> iterator(a);
}
Copy the code

We’ll have to keep looking for its implementation, which can be found in the MusicProvider constructor:

//MusicProvider.java
public MusicProvider(a) {
    this(new RemoteJSONSource());
}
public MusicProvider(MusicProviderSource source) { mSource = source; . }Copy the code

The RemoteJSONSource class does the final connecting to the server and retrieving the data. Let’s look at how it overrides the iterator method:

//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(a) {
        try {
            int slashPos = CATALOG_URL.lastIndexOf('/');
            String path = CATALOG_URL.substring(0, slashPos + 1);
            JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);// Download the JSON file
            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); }}/** * Parses the JSON data to build the MediaMetadata object */
    private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath) throws JSONException {... }/** * Download the JSON file from the server, parse and return the JSON object */
    private JSONObject fetchJSONFromUrl(String urlString) throws JSONException {... }}Copy the code

The code is not complicated, and the whole process can be summarized as follows: get the JSON file that encapsulates the music source information from the server according to the URL → parse the JSON object and build it into the MediaMetadata object → add all the data to the list set and return it to the MusicProvider, and then the data acquisition is complete


Build an audio collection divided by type

We went back to MusicProvider retrieveMedia method. The second step is to iterate over the previously retrieved iterator data, extract the individual MediaMetadata objects, and reinsert them as key-value pairs into the mMusicListById collection

//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));
}
Copy the code

MMusicListById is of type ConcurrentHashMap, as you can see from the MusicProvider constructor

//MusicProvider.java
private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
public MusicProvider(MusicProviderSource source) {... mMusicListById =new ConcurrentHashMap<>();
}
Copy the code

After saving all the data to the mMusicListById collection, call the buildListsByGenre method to re-partition the data by music type and store it in the mMusicListByGenre collection (note the comparison of Map value types) :

//MusicProvider.java
private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByGenre;
public MusicProvider(MusicProviderSource source) {... mMusicListByGenre =new ConcurrentHashMap<>();
}
private synchronized void buildListsByGenre(a) {
    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;
}
Copy the code

Analyze the logic behind buildListsByGenre: Iterate over the audio elements of mMusicListById and use genre of the audio as the key to find the corresponding list in the temporary collection of newMusicListByGenre. If this list is empty, it proves that the audio of this type has not been previously saved into newMusicListByGenre. Create an empty list to hold the currently iterated audio elements and build key-value pairs using genre as the key value. When iterating through to the next element, newMusicListByGenre will simply save the element into the list if it already has a list of sounds for that type. In this way, all the audio data can be divided into lists by type in a single walk, and the client can select the queue to play by audio type


Update list display data

After buildListsByGenre is done, set the appropriate state, the asynchronous task in retrieveMediaAsync, the doInBackground of the AsyncTask is done, and then the callback is done in onPostExecute, Go back to MusicService and send the data to the client

//MusicService.java
@Override
public void onLoadChildren(@NonNull final String parentMediaId,
                           @NonNull final Result<List<MediaItem>> result) {... mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
        // Complete the callback after the music is loaded
        @Override
        public void onMusicCatalogReady(boolean success) { result.sendResult(mMusicProvider.getChildren(parentMediaId, getResources())); }}); }Copy the code

The client (MediaBrowserFragment) gets the data and refreshes the list Adapter to display the content to the user

//MediaBrowserFragment.java
private 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); }}};Copy the code

MusicProviderOther features

As a content provider, MusicProvider certainly does more than that. MusicProvider supports out-of-order audio playback, mainly through the collections.shuffle method:

//MusicProvider.java
public Iterable<MediaMetadataCompat> getShuffledMusic(a) {
    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);// Shuffle the list order
    return shuffled;
}
Copy the code

Support for personal “likes”, i.e. favorites:

//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); }}/** * Check if the music is in the "like" list */
public boolean isFavorite(String musicId) {
    return mFavoriteTracks.contains(musicId);
}
Copy the code

In addition, it supports a variety of simple retrieval functions:

//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;
}
Copy the code

So UAMP player data management aspects of the content to this temporarily concluded, the subsequent may pick some of the UAMP tool classes. Finally, the convention: if there are any omissions or suggestions welcome to leave a comment, if you think the blogger wrote good, please point a thumbs-up, your support is my biggest motivation ~