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 }