blob: dd89c2d30ceb8e00a8691a322e9740fb4680765c [file] [log] [blame]
/*
* 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
}
}
}
}
}