Skip to content

Most visited

Recently visited

navigation
MediaBrowserService / src / com.example.android.mediabrowserservice / model /

MusicProvider.java

1
/*
2
 * Copyright (C) 2014 The Android Open Source Project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.example.android.mediabrowserservice.model;
18
 
19
import android.media.MediaMetadata;
20
import android.os.AsyncTask;
21
 
22
import com.example.android.mediabrowserservice.utils.LogHelper;
23
 
24
import org.json.JSONArray;
25
import org.json.JSONException;
26
import org.json.JSONObject;
27
 
28
import java.io.BufferedInputStream;
29
import java.io.BufferedReader;
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.io.InputStreamReader;
33
import java.net.URL;
34
import java.net.URLConnection;
35
import java.util.ArrayList;
36
import java.util.Collections;
37
import java.util.List;
38
import java.util.Set;
39
import java.util.concurrent.ConcurrentHashMap;
40
import java.util.concurrent.ConcurrentMap;
41
 
42
/**
43
 * Utility class to get a list of MusicTrack's based on a server-side JSON
44
 * configuration.
45
 */
46
public class MusicProvider {
47
 
48
    private static final String TAG = LogHelper.makeLogTag(MusicProvider.class);
49
 
50
    private static final String CATALOG_URL =
51
        "http://storage.googleapis.com/automotive-media/music.json";
52
 
53
    public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
54
 
55
    private static final String JSON_MUSIC = "music";
56
    private static final String JSON_TITLE = "title";
57
    private static final String JSON_ALBUM = "album";
58
    private static final String JSON_ARTIST = "artist";
59
    private static final String JSON_GENRE = "genre";
60
    private static final String JSON_SOURCE = "source";
61
    private static final String JSON_IMAGE = "image";
62
    private static final String JSON_TRACK_NUMBER = "trackNumber";
63
    private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
64
    private static final String JSON_DURATION = "duration";
65
 
66
    // Categorized caches for music track data:
67
    private ConcurrentMap<String, List<MediaMetadata>> mMusicListByGenre;
68
    private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
69
 
70
    private final Set<String> mFavoriteTracks;
71
 
72
    enum State {
73
        NON_INITIALIZED, INITIALIZING, INITIALIZED
74
    }
75
 
76
    private volatile State mCurrentState = State.NON_INITIALIZED;
77
 
78
    public interface Callback {
79
        void onMusicCatalogReady(boolean success);
80
    }
81
 
82
    public MusicProvider() {
83
        mMusicListByGenre = new ConcurrentHashMap<>();
84
        mMusicListById = new ConcurrentHashMap<>();
85
        mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
86
    }
87
 
88
    /**
89
     * Get an iterator over the list of genres
90
     *
91
     * @return genres
92
     */
93
    public Iterable<String> getGenres() {
94
        if (mCurrentState != State.INITIALIZED) {
95
            return Collections.emptyList();
96
        }
97
        return mMusicListByGenre.keySet();
98
    }
99
 
100
    /**
101
     * Get music tracks of the given genre
102
     *
103
     */
104
    public Iterable<MediaMetadata> getMusicsByGenre(String genre) {
105
        if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
106
            return Collections.emptyList();
107
        }
108
        return mMusicListByGenre.get(genre);
109
    }
110
 
111
    /**
112
     * Very basic implementation of a search that filter music tracks which title containing
113
     * the given query.
114
     *
115
     */
116
    public Iterable<MediaMetadata> searchMusic(String titleQuery) {
117
        if (mCurrentState != State.INITIALIZED) {
118
            return Collections.emptyList();
119
        }
120
        ArrayList<MediaMetadata> result = new ArrayList<>();
121
        titleQuery = titleQuery.toLowerCase();
122
        for (MutableMediaMetadata track : mMusicListById.values()) {
123
            if (track.metadata.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
124
                    .contains(titleQuery)) {
125
                result.add(track.metadata);
126
            }
127
        }
128
        return result;
129
    }
130
 
131
    /**
132
     * Return the MediaMetadata for the given musicID.
133
     *
134
     * @param musicId The unique, non-hierarchical music ID.
135
     */
136
    public MediaMetadata getMusic(String musicId) {
137
        return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null;
138
    }
139
 
140
    public synchronized void updateMusic(String musicId, MediaMetadata metadata) {
141
        MutableMediaMetadata track = mMusicListById.get(musicId);
142
        if (track == null) {
143
            return;
144
        }
145
 
146
        String oldGenre = track.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
147
        String newGenre = metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
148
 
149
        track.metadata = metadata;
150
 
151
        // if genre has changed, we need to rebuild the list by genre
152
        if (!oldGenre.equals(newGenre)) {
153
            buildListsByGenre();
154
        }
155
    }
156
 
157
    public void setFavorite(String musicId, boolean favorite) {
158
        if (favorite) {
159
            mFavoriteTracks.add(musicId);
160
        } else {
161
            mFavoriteTracks.remove(musicId);
162
        }
163
    }
164
 
165
    public boolean isFavorite(String musicId) {
166
        return mFavoriteTracks.contains(musicId);
167
    }
168
 
169
    public boolean isInitialized() {
170
        return mCurrentState == State.INITIALIZED;
171
    }
172
 
173
    /**
174
     * Get the list of music tracks from a server and caches the track information
175
     * for future reference, keying tracks by musicId and grouping by genre.
176
     */
177
    public void retrieveMediaAsync(final Callback callback) {
178
        LogHelper.d(TAG, "retrieveMediaAsync called");
179
        if (mCurrentState == State.INITIALIZED) {
180
            // Nothing to do, execute callback immediately
181
            callback.onMusicCatalogReady(true);
182
            return;
183
        }
184
 
185
        // Asynchronously load the music catalog in a separate thread
186
        new AsyncTask<Void, Void, State>() {
187
            @Override
188
            protected State doInBackground(Void... params) {
189
                retrieveMedia();
190
                return mCurrentState;
191
            }
192
 
193
            @Override
194
            protected void onPostExecute(State current) {
195
                if (callback != null) {
196
                    callback.onMusicCatalogReady(current == State.INITIALIZED);
197
                }
198
            }
199
        }.execute();
200
    }
201
 
202
    private synchronized void buildListsByGenre() {
203
        ConcurrentMap<String, List<MediaMetadata>> newMusicListByGenre = new ConcurrentHashMap<>();
204
 
205
        for (MutableMediaMetadata m : mMusicListById.values()) {
206
            String genre = m.metadata.getString(MediaMetadata.METADATA_KEY_GENRE);
207
            List<MediaMetadata> list = newMusicListByGenre.get(genre);
208
            if (list == null) {
209
                list = new ArrayList<>();
210
                newMusicListByGenre.put(genre, list);
211
            }
212
            list.add(m.metadata);
213
        }
214
        mMusicListByGenre = newMusicListByGenre;
215
    }
216
 
217
    private synchronized void retrieveMedia() {
218
        try {
219
            if (mCurrentState == State.NON_INITIALIZED) {
220
                mCurrentState = State.INITIALIZING;
221
 
222
                int slashPos = CATALOG_URL.lastIndexOf('/');
223
                String path = CATALOG_URL.substring(0, slashPos + 1);
224
                JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);
225
                if (jsonObj == null) {
226
                    return;
227
                }
228
                JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
229
                if (tracks != null) {
230
                    for (int j = 0; j < tracks.length(); j++) {
231
                        MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path);
232
                        String musicId = item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
233
                        mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));
234
                    }
235
                    buildListsByGenre();
236
                }
237
                mCurrentState = State.INITIALIZED;
238
            }
239
        } catch (JSONException e) {
240
            LogHelper.e(TAG, e, "Could not retrieve music list");
241
        } finally {
242
            if (mCurrentState != State.INITIALIZED) {
243
                // Something bad happened, so we reset state to NON_INITIALIZED to allow
244
                // retries (eg if the network connection is temporary unavailable)
245
                mCurrentState = State.NON_INITIALIZED;
246
            }
247
        }
248
    }
249
 
250
    private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException {
251
        String title = json.getString(JSON_TITLE);
252
        String album = json.getString(JSON_ALBUM);
253
        String artist = json.getString(JSON_ARTIST);
254
        String genre = json.getString(JSON_GENRE);
255
        String source = json.getString(JSON_SOURCE);
256
        String iconUrl = json.getString(JSON_IMAGE);
257
        int trackNumber = json.getInt(JSON_TRACK_NUMBER);
258
        int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
259
        int duration = json.getInt(JSON_DURATION) * 1000; // ms
260
 
261
        LogHelper.d(TAG, "Found music track: ", json);
262
 
263
        // Media is stored relative to JSON file
264
        if (!source.startsWith("http")) {
265
            source = basePath + source;
266
        }
267
        if (!iconUrl.startsWith("http")) {
268
            iconUrl = basePath + iconUrl;
269
        }
270
        // Since we don't have a unique ID in the server, we fake one using the hashcode of
271
        // the music source. In a real world app, this could come from the server.
272
        String id = String.valueOf(source.hashCode());
273
 
274
        // Adding the music source to the MediaMetadata (and consequently using it in the
275
        // mediaSession.setMetadata) is not a good idea for a real world music app, because
276
        // the session metadata can be accessed by notification listeners. This is done in this
277
        // sample for convenience only.
278
        return new MediaMetadata.Builder()
279
                .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id)
280
                .putString(CUSTOM_METADATA_TRACK_SOURCE, source)
281
                .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
282
                .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
283
                .putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
284
                .putString(MediaMetadata.METADATA_KEY_GENRE, genre)
285
                .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl)
286
                .putString(MediaMetadata.METADATA_KEY_TITLE, title)
287
                .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber)
288
                .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount)
289
                .build();
290
    }
291
 
292
    /**
293
     * Download a JSON file from a server, parse the content and return the JSON
294
     * object.
295
     *
296
     * @return result JSONObject containing the parsed representation.
297
     */
298
    private JSONObject fetchJSONFromUrl(String urlString) {
299
        InputStream is = null;
300
        try {
301
            URL url = new URL(urlString);
302
            URLConnection urlConnection = url.openConnection();
303
            is = new BufferedInputStream(urlConnection.getInputStream());
304
            BufferedReader reader = new BufferedReader(new InputStreamReader(