summaryrefslogtreecommitdiff
path: root/MusicDemo/src/main/java/com/example/android/musicservicedemo/model/MusicProvider.java
diff options
context:
space:
mode:
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.java296
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