• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onMusicCatalogReady(boolean success)79          void onMusicCatalogReady(boolean success);
80      }
81  
MusicProvider()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       */
getGenres()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       */
getMusicsByGenre(String genre)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       */
searchMusic(String titleQuery)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       */
getMusic(String musicId)136      public MediaMetadata getMusic(String musicId) {
137          return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null;
138      }
139  
updateMusic(String musicId, MediaMetadata metadata)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  
setFavorite(String musicId, boolean favorite)157      public void setFavorite(String musicId, boolean favorite) {
158          if (favorite) {
159              mFavoriteTracks.add(musicId);
160          } else {
161              mFavoriteTracks.remove(musicId);
162          }
163      }
164  
isFavorite(String musicId)165      public boolean isFavorite(String musicId) {
166          return mFavoriteTracks.contains(musicId);
167      }
168  
isInitialized()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       */
retrieveMediaAsync(final Callback callback)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  
buildListsByGenre()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  
retrieveMedia()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  
buildFromJSON(JSONObject json, String basePath)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       */
fetchJSONFromUrl(String urlString)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(
305                      urlConnection.getInputStream(), "iso-8859-1"));
306              StringBuilder sb = new StringBuilder();
307              String line;
308              while ((line = reader.readLine()) != null) {
309                  sb.append(line);
310              }
311              return new JSONObject(sb.toString());
312          } catch (Exception e) {
313              LogHelper.e(TAG, "Failed to parse the json for media list", e);
314              return null;
315          } finally {
316              if (is != null) {
317                  try {
318                      is.close();
319                  } catch (IOException e) {
320                      // ignore
321                  }
322              }
323          }
324      }
325  }
326