diff options
Diffstat (limited to 'MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java')
-rw-r--r-- | MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java | 296 |
1 files changed, 296 insertions, 0 deletions
diff --git a/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java b/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java new file mode 100644 index 0000000..dd89c2d --- /dev/null +++ b/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.musicservicedemo.model; + +import android.media.MediaMetadata; +import android.os.AsyncTask; + +import com.example.android.musicservicedemo.utils.LogHelper; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to get a list of MusicTrack's based on a server-side JSON + * configuration. + */ +public class MusicProvider { + + private static final String TAG = "MusicProvider"; + + private static final String CATALOG_URL = "http://storage.googleapis.com/automotive-media/music.json"; + + public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__"; + + private static String JSON_MUSIC = "music"; + private static String JSON_TITLE = "title"; + private static String JSON_ALBUM = "album"; + private static String JSON_ARTIST = "artist"; + private static String JSON_GENRE = "genre"; + private static String JSON_SOURCE = "source"; + private static String JSON_IMAGE = "image"; + private static String JSON_TRACK_NUMBER = "trackNumber"; + private static String JSON_TOTAL_TRACK_COUNT = "totalTrackCount"; + private static String JSON_DURATION = "duration"; + + private final ReentrantLock initializationLock = new ReentrantLock(); + + // Categorized caches for music track data: + private final HashMap<String, List<MediaMetadata>> mMusicListByGenre; + private final HashMap<String, MediaMetadata> mMusicListById; + + private final HashSet<String> mFavoriteTracks; + + enum State { + NON_INITIALIZED, INITIALIZING, INITIALIZED; + } + + private State mCurrentState = State.NON_INITIALIZED; + + + public interface Callback { + void onMusicCatalogReady(boolean success); + } + + public MusicProvider() { + mMusicListByGenre = new HashMap<>(); + mMusicListById = new HashMap<>(); + mFavoriteTracks = new HashSet<>(); + } + + /** + * Get an iterator over the list of genres + * + * @return + */ + public Iterable<String> getGenres() { + if (mCurrentState != State.INITIALIZED) { + return new ArrayList<String>(0); + } + return mMusicListByGenre.keySet(); + } + + /** + * Get music tracks of the given genre + * + * @return + */ + public Iterable<MediaMetadata> getMusicsByGenre(String genre) { + if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) { + return new ArrayList<MediaMetadata>(); + } + return mMusicListByGenre.get(genre); + } + + /** + * Very basic implementation of a search that filter music tracks which title containing + * the given query. + * + * @return + */ + public Iterable<MediaMetadata> searchMusics(String titleQuery) { + ArrayList<MediaMetadata> result = new ArrayList<>(); + if (mCurrentState != State.INITIALIZED) { + return result; + } + titleQuery = titleQuery.toLowerCase(); + for (MediaMetadata track: mMusicListById.values()) { + if (track.getString(MediaMetadata.METADATA_KEY_TITLE).toLowerCase() + .contains(titleQuery)) { + result.add(track); + } + } + return result; + } + + public MediaMetadata getMusic(String mediaId) { + return mMusicListById.get(mediaId); + } + + public void setFavorite(String mediaId, boolean favorite) { + if (favorite) { + mFavoriteTracks.add(mediaId); + } else { + mFavoriteTracks.remove(mediaId); + } + } + + public boolean isFavorite(String musicId) { + return mFavoriteTracks.contains(musicId); + } + + public boolean isInitialized() { + return mCurrentState == State.INITIALIZED; + } + + /** + * Get the list of music tracks from a server and caches the track information + * for future reference, keying tracks by mediaId and grouping by genre. + * + * @return + */ + public void retrieveMedia(final Callback callback) { + + if (mCurrentState == State.INITIALIZED) { + // Nothing to do, execute callback immediately + callback.onMusicCatalogReady(true); + return; + } + + // Asynchronously load the music catalog in a separate thread + new AsyncTask() { + @Override + protected Object doInBackground(Object[] objects) { + retrieveMediaAsync(callback); + return null; + } + }.execute(); + } + + private void retrieveMediaAsync(Callback callback) { + initializationLock.lock(); + + try { + if (mCurrentState == State.NON_INITIALIZED) { + mCurrentState = State.INITIALIZING; + + int slashPos = CATALOG_URL.lastIndexOf('/'); + String path = CATALOG_URL.substring(0, slashPos + 1); + JSONObject jsonObj = parseUrl(CATALOG_URL); + + JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); + if (tracks != null) { + for (int j = 0; j < tracks.length(); j++) { + MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path); + String genre = item.getString(MediaMetadata.METADATA_KEY_GENRE); + List<MediaMetadata> list = mMusicListByGenre.get(genre); + if (list == null) { + list = new ArrayList<>(); + } + list.add(item); + mMusicListByGenre.put(genre, list); + mMusicListById.put(item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID), + item); + } + } + mCurrentState = State.INITIALIZED; + } + } catch (RuntimeException | JSONException e) { + LogHelper.e(TAG, e, "Could not retrieve music list"); + } finally { + if (mCurrentState != State.INITIALIZED) { + // Something bad happened, so we reset state to NON_INITIALIZED to allow + // retries (eg if the network connection is temporary unavailable) + mCurrentState = State.NON_INITIALIZED; + } + initializationLock.unlock(); + if (callback != null) { + callback.onMusicCatalogReady(mCurrentState == State.INITIALIZED); + } + } + } + + private MediaMetadata buildFromJSON(JSONObject json, String basePath) throws JSONException { + String title = json.getString(JSON_TITLE); + String album = json.getString(JSON_ALBUM); + String artist = json.getString(JSON_ARTIST); + String genre = json.getString(JSON_GENRE); + String source = json.getString(JSON_SOURCE); + String iconUrl = json.getString(JSON_IMAGE); + int trackNumber = json.getInt(JSON_TRACK_NUMBER); + int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT); + int duration = json.getInt(JSON_DURATION) * 1000; // ms + + LogHelper.d(TAG, "Found music track: ", json); + + // Media is stored relative to JSON file + if (!source.startsWith("http")) { + source = basePath + source; + } + if (!iconUrl.startsWith("http")) { + iconUrl = basePath + iconUrl; + } + // Since we don't have a unique ID in the server, we fake one using the hashcode of + // the music source. In a real world app, this could come from the server. + String id = String.valueOf(source.hashCode()); + + // Adding the music source to the MediaMetadata (and consequently using it in the + // mediaSession.setMetadata) is not a good idea for a real world music app, because + // the session metadata can be accessed by notification listeners. This is done in this + // sample for convenience only. + return new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, id) + .putString(CUSTOM_METADATA_TRACK_SOURCE, source) + .putString(MediaMetadata.METADATA_KEY_ALBUM, album) + .putString(MediaMetadata.METADATA_KEY_ARTIST, artist) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration) + .putString(MediaMetadata.METADATA_KEY_GENRE, genre) + .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, iconUrl) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, trackNumber) + .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, totalTrackCount) + .build(); + } + + /** + * Download a JSON file from a server, parse the content and return the JSON + * object. + * + * @param urlString + * @return + */ + private JSONObject parseUrl(String urlString) { + InputStream is = null; + try { + java.net.URL url = new java.net.URL(urlString); + URLConnection urlConnection = url.openConnection(); + is = new BufferedInputStream(urlConnection.getInputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader( + urlConnection.getInputStream(), "iso-8859-1"), 8); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + return new JSONObject(sb.toString()); + } catch (Exception e) { + LogHelper.e(TAG, "Failed to parse the json for media list", e); + return null; + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + } + } +}
\ No newline at end of file |