• Home
  • 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.URLConnection;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.concurrent.locks.ReentrantLock;
39 
40 /**
41  * Utility class to get a list of MusicTrack's based on a server-side JSON
42  * configuration.
43  */
44 public class MusicProvider {
45 
46     private static final String TAG = "MusicProvider";
47 
48     private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json";
49 
50     public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
51 
52     private static String JSON_MUSIC = "music";
53     private static String JSON_TITLE = "title";
54     private static String JSON_ALBUM = "album";
55     private static String JSON_ARTIST = "artist";
56     private static String JSON_GENRE = "genre";
57     private static String JSON_SOURCE = "source";
58     private static String JSON_IMAGE = "image";
59     private static String JSON_TRACK_NUMBER = "trackNumber";
60     private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
61     private static String JSON_DURATION = "duration";
62 
63     private final ReentrantLock initializationLock = new ReentrantLock();
64 
65     // Categorized caches for music track data:
66     private final HashMap<String, List<MediaMetadata>> mMusicListByGenre;
67     private final HashMap<String, MediaMetadata> mMusicListById;
68 
69     private final HashSet<String> mFavoriteTracks;
70 
71     enum State {
72         NON_INITIALIZED, INITIALIZING, INITIALIZED;
73     }
74 
75     private State mCurrentState = State.NON_INITIALIZED;
76 
77 
78     public interface Callback {
onMusicCatalogReady(boolean success)79         void onMusicCatalogReady(boolean success);
80     }
81 
MusicProvider()82     public MusicProvider() {
83         mMusicListByGenre = new HashMap<>();
84         mMusicListById = new HashMap<>();
85         mFavoriteTracks = new HashSet<>();
86     }
87 
88     /**
89      * Get an iterator over the list of genres
90      *
91      * @return
92      */
getGenres()93     public Iterable<String> getGenres() {
94         if (mCurrentState != State.INITIALIZED) {
95             return new ArrayList<String>(0);
96         }
97         return mMusicListByGenre.keySet();
98     }
99 
100     /**
101      * Get music tracks of the given genre
102      *
103      * @return
104      */
getMusicsByGenre(String genre)105     public Iterable<MediaMetadata> getMusicsByGenre(String genre) {
106         if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
107             return new ArrayList<MediaMetadata>();
108         }
109         return mMusicListByGenre.get(genre);
110     }
111 
112     /**
113      * Very basic implementation of a search that filter music tracks which title containing
114      * the given query.
115      *
116      * @return
117      */
searchMusics(String titleQuery)118     public Iterable<MediaMetadata> searchMusics(String titleQuery) {
119         ArrayList<MediaMetadata> result = new ArrayList<>();
120         if (mCurrentState != State.INITIALIZED) {
121             return result;
122         }
123         titleQuery = titleQuery.toLowerCase();
124         for (MediaMetadata track: mMusicListById.values()) {
125             if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase()
126                     .contains(titleQuery)) {
127                 result.add(track);
128             }
129         }
130         return result;
131     }
132 
getMusic(String mediaId)133     public MediaMetadata getMusic(String mediaId) {
134         return mMusicListById.get(mediaId);
135     }
136 
setFavorite(String mediaId, boolean favorite)137     public void setFavorite(String mediaId, boolean favorite) {
138         if (favorite) {
139             mFavoriteTracks.add(mediaId);
140         } else {
141             mFavoriteTracks.remove(mediaId);
142         }
143     }
144 
isFavorite(String musicId)145     public boolean isFavorite(String musicId) {
146         return mFavoriteTracks.contains(musicId);
147     }
148 
isInitialized()149     public boolean isInitialized() {
150         return mCurrentState == State.INITIALIZED;
151     }
152 
153     /**
154      * Get the list of music tracks from a server and caches the track information
155      * for future reference, keying tracks by mediaId and grouping by genre.
156      *
157      * @return
158      */
retrieveMedia(final Callback callback)159     public void retrieveMedia(final Callback callback) {
160 
161         if (mCurrentState == State.INITIALIZED) {
162             // Nothing to do, execute callback immediately
163             callback.onMusicCatalogReady(true);
164             return;
165         }
166 
167         // Asynchronously load the music catalog in a separate thread
168         new AsyncTask() {
169             @Override
170             protected Object doInBackground(Object[] objects) {
171                 retrieveMediaAsync(callback);
172                 return null;
173             }
174         }.execute();
175     }
176 
retrieveMediaAsync(Callback callback)177     private void retrieveMediaAsync(Callback callback) {
178         initializationLock.lock();
179 
180         try {
181             if (mCurrentState == State.NON_INITIALIZED) {
182                 mCurrentState = State.INITIALIZING;
183 
184                 int slashPos = CATALOG_URL.lastIndexOf('/');
185                 String path = CATALOG_URL.substring(0, slashPos + 1);
186                 JSONObject jsonObj = parseUrl(CATALOG_URL);
187 
188                 JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
189                 if (tracks != null) {
190                     for (int j = 0; j < tracks.length(); j++) {
191                         MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path);
192                         String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE);
193                         List<MediaMetadata> list = mMusicListByGenre.get(genre);
194                         if (list == null) {
195                             list = new ArrayList<>();
196                         }
197                         list.add(item);
198                         mMusicListByGenre.put(genre, list);
199                         mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID),
200                                 item);
201                     }
202                 }
203                 mCurrentState = State.INITIALIZED;
204             }
205         } catch (RuntimeException | JSONException e) {
206             LogHelper.e(TAG, e, "Could not retrieve music list");
207         } finally {
208             if (mCurrentState != State.INITIALIZED) {
209                 // Something bad happened, so we reset state to NON_INITIALIZED to allow
210                 // retries (eg if the network connection is temporary unavailable)
211                 mCurrentState = State.NON_INITIALIZED;
212             }
213             initializationLock.unlock();
214             if (callback != null) {
215                 callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED);
216             }
217         }
218     }
219 
buildFromJSON(JSONObject json, String basePath)220     private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException {
221         String title = json.getString(JSON_TITLE);
222         String album = json.getString(JSON_ALBUM);
223         String artist = json.getString(JSON_ARTIST);
224         String genre = json.getString(JSON_GENRE);
225         String source = json.getString(JSON_SOURCE);
226         String iconUrl = json.getString(JSON_IMAGE);
227         int trackNumber = json.getInt(JSON_TRACK_NUMBER);
228         int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
229         int duration = json.getInt(JSON_DURATION) * 1000; // ms
230 
231         LogHelper.d(TAG, "Found music track: ", json);
232 
233         // Media is stored relative to JSON file
234         if (!source.startsWith("http")) {
235             source = basePath + source;
236         }
237         if (!iconUrl.startsWith("http")) {
238             iconUrl = basePath + iconUrl;
239         }
240         // Since we don't have a unique ID in the server, we fake one using the hashcode of
241         // the music source. In a real world app, this could come from the server.
242         String id = String.valueOf(source.hashCode());
243 
244         // Adding the music source to the MediaMetadata (and consequently using it in the
245         // mediaSession.setMetadata) is not a good idea for a real world music app, because
246         // the session metadata can be accessed by notification listeners. This is done in this
247         // sample for convenience only.
248         return new MediaMetadata.Builder()
249                 .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id)
250                 .putString(CUSTOM_METADATA_TRACK_SOURCE, source)
251                 .putString(MediaMetadata.METADATA_KEY_ALBUM, album)
252                 .putString(MediaMetadata.METADATA_KEY_ARTIST, artist)
253                 .putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
254                 .putString(MediaMetadata.METADATA_KEY_GENRE, genre)
255                 .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl)
256                 .putString(MediaMetadata.METADATA_KEY_TITLE, title)
257                 .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber)
258                 .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount)
259                 .build();
260     }
261 
262     /**
263      * Download a JSON file from a server, parse the content and return the JSON
264      * object.
265      *
266      * @param urlString
267      * @return
268      */
parseUrl(String urlString)269     private JSONObject parseUrl(String urlString) {
270         InputStream is = null;
271         try {
272             java.net.URL url = new java.net.URL(urlString);
273             URLConnection urlConnection = url.openConnection();
274             is = new BufferedInputStream(urlConnection.getInputStream());
275             BufferedReader reader = new BufferedReader(new InputStreamReader(
276                     urlConnection.getInputStream(), "iso-8859-1"));
277             StringBuilder sb = new StringBuilder();
278             String line = null;
279             while ((line = reader.readLine()) != null) {
280                 sb.append(line);
281             }
282             return new JSONObject(sb.toString());
283         } catch (Exception e) {
284             LogHelper.e(TAG, "Failed to parse the json for media list", e);
285             return null;
286         } finally {
287             if (is != null) {
288                 try {
289                     is.close();
290                 } catch (IOException e) {
291                     // ignore
292                 }
293             }
294         }
295     }
296 }