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.os.AsyncTask; 20 import android.support.v4.media.MediaMetadataCompat; 21 import android.util.Log; 22 23 import org.json.JSONArray; 24 import org.json.JSONException; 25 import org.json.JSONObject; 26 27 import java.io.BufferedInputStream; 28 import java.io.BufferedReader; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.net.URL; 33 import java.net.URLConnection; 34 import java.util.Collections; 35 import java.util.LinkedHashMap; 36 37 /** 38 * Utility class to get a list of MusicTrack's based on a server-side JSON 39 * configuration. 40 * 41 * In a real application this class may pull data from a remote server, as we do here, 42 * or potentially use {@link android.provider.MediaStore} to locate media files located on 43 * the device. 44 */ 45 public class MusicProvider { 46 47 private static final String TAG = MusicProvider.class.getSimpleName(); 48 49 public static final String MEDIA_ID_ROOT = "__ROOT__"; 50 public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY__"; 51 52 private static final String CATALOG_URL = 53 "https://storage.googleapis.com/automotive-media/music.json"; 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 final LinkedHashMap<String, MediaMetadataCompat> mMusicListById; 68 69 private enum State { 70 NON_INITIALIZED, INITIALIZING, INITIALIZED 71 } 72 73 private volatile State mCurrentState = State.NON_INITIALIZED; 74 75 /** 76 * Callback used by MusicService. 77 */ 78 public interface Callback { 79 void onMusicCatalogReady(boolean success); 80 } 81 82 public MusicProvider() { 83 mMusicListById = new LinkedHashMap<>(); 84 } 85 86 public Iterable<MediaMetadataCompat> getAllMusics() { 87 if (mCurrentState != State.INITIALIZED || mMusicListById.isEmpty()) { 88 return Collections.emptyList(); 89 } 90 return mMusicListById.values(); 91 } 92 93 /** 94 * Return the MediaMetadata for the given musicID. 95 * 96 * @param musicId The unique music ID. 97 */ 98 public MediaMetadataCompat getMusic(String musicId) { 99 return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null; 100 } 101 102 /** 103 * Update the metadata associated with a musicId. If the musicId doesn't exist, the 104 * update is dropped. (That is, it does not create a new mediaId.) 105 * @param musicId The ID 106 * @param metadata New Metadata to associate with it 107 */ 108 public synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) { 109 MediaMetadataCompat track = mMusicListById.get(musicId); 110 if (track != null) { 111 mMusicListById.put(musicId, metadata); 112 } 113 } 114 115 public boolean isInitialized() { 116 return mCurrentState == State.INITIALIZED; 117 } 118 119 /** 120 * Get the list of music tracks from a server and caches the track information 121 * for future reference, keying tracks by musicId and grouping by genre. 122 */ 123 public void retrieveMediaAsync(final Callback callback) { 124 Log.d(TAG, "retrieveMediaAsync called"); 125 if (mCurrentState == State.INITIALIZED) { 126 // Already initialized, so call back immediately. 127 callback.onMusicCatalogReady(true); 128 return; 129 } 130 131 // Asynchronously load the music catalog in a separate thread 132 new AsyncTask<Void, Void, State>() { 133 @Override 134 protected State doInBackground(Void... params) { 135 retrieveMedia(); 136 return mCurrentState; 137 } 138 139 @Override 140 protected void onPostExecute(State current) { 141 if (callback != null) { 142 callback.onMusicCatalogReady(current == State.INITIALIZED); 143 } 144 } 145 }.execute(); 146 } 147 148 private synchronized void retrieveMedia() { 149 try { 150 if (mCurrentState == State.NON_INITIALIZED) { 151 mCurrentState = State.INITIALIZING; 152 153 int slashPos = CATALOG_URL.lastIndexOf('/'); 154 String path = CATALOG_URL.substring(0, slashPos + 1); 155 JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL); 156 if (jsonObj == null) { 157 return; 158 } 159 JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); 160 if (tracks != null) { 161 for (int j = tracks.length() - 1; j >= 0; j--) { 162 MediaMetadataCompat item = buildFromJSON(tracks.getJSONObject(j), path); 163 String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID); 164 mMusicListById.put(musicId, item); 165 } 166 } 167 mCurrentState = State.INITIALIZED; 168 } 169 } catch (JSONException jsonException) { 170 Log.e(TAG, "Could not retrieve music list", jsonException); 171 } finally { 172 if (mCurrentState != State.INITIALIZED) { 173 // Something bad happened, so we reset state to NON_INITIALIZED to allow 174 // retries (eg if the network connection is temporary unavailable) 175 mCurrentState = State.NON_INITIALIZED; 176 } 177 } 178 } 179 180 private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath) 181 throws JSONException { 182 183 String title = json.getString(JSON_TITLE); 184 String album = json.getString(JSON_ALBUM); 185 String artist = json.getString(JSON_ARTIST); 186 String genre = json.getString(JSON_GENRE); 187 String source = json.getString(JSON_SOURCE); 188 String iconUrl = json.getString(JSON_IMAGE); 189 int trackNumber = json.getInt(JSON_TRACK_NUMBER); 190 int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT); 191 int duration = json.getInt(JSON_DURATION) * 1000; // ms 192 193 Log.d(TAG, "Found music track: " + json); 194 195 // Media is stored relative to JSON file 196 if (!source.startsWith("https")) { 197 source = basePath + source; 198 } 199 if (!iconUrl.startsWith("https")) { 200 iconUrl = basePath + iconUrl; 201 } 202 // Since we don't have a unique ID in the server, we fake one using the hashcode of 203 // the music source. In a real world app, this could come from the server. 204 String id = String.valueOf(source.hashCode()); 205 206 // Adding the music source to the MediaMetadata (and consequently using it in the 207 // mediaSession.setMetadata) is not a good idea for a real world music app, because 208 // the session metadata can be accessed by notification listeners. This is done in this 209 // sample for convenience only. 210 return new MediaMetadataCompat.Builder() 211 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) 212 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, source) 213 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) 214 .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) 215 .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration) 216 .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre) 217 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl) 218 .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) 219 .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber) 220 .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount) 221 .build(); 222 } 223 224 /** 225 * Download a JSON file from a server, parse the content and return the JSON 226 * object. 227 * 228 * @return result JSONObject containing the parsed representation. 229 */ 230 private JSONObject fetchJSONFromUrl(String urlString) { 231 InputStream inputStream = null; 232 try { 233 URL url = new URL(urlString); 234 URLConnection urlConnection = url.openConnection(); 235 inputStream = new BufferedInputStream(urlConnection.getInputStream()); 236 BufferedReader reader = new BufferedReader(new InputStreamReader( 237 urlConnection.getInputStream(), "iso-8859-1")); 238 StringBuilder stringBuilder = new StringBuilder(); 239 String line; 240 while ((line = reader.readLine()) != null) { 241 stringBuilder.append(line); 242 } 243 return new JSONObject(stringBuilder.toString()); 244 } catch (IOException | JSONException exception) { 245 Log.e(TAG, "Failed to parse the json for media list", exception); 246 return null; 247 } finally { 248 // If the inputStream was opened, try to close it now. 249 if (inputStream != null) { 250 try { 251 inputStream.close(); 252 } catch (IOException ignored) { 253 // Ignore the exception since there's nothing left to do with the stream 254 } 255 } 256 } 257 } 258 } 259