package com.markzhai.lyrichere.provider; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.provider.MediaStore; import android.support.v4.media.MediaMetadataCompat; import com.markzhai.lyrichere.app.LHApplication; import com.markzhai.lyrichere.R; import com.markzhai.lyrichere.data.MutableMediaMetadata; import com.markzhai.lyrichere.utils.LogUtils; import com.markzhai.lyrichere.utils.MusicUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Utility class to get a list of MusicTrack's based on a server-side JSON configuration or * local filesystem. */ public class MusicProvider { private static final String TAG = LogUtils.makeLogTag(MusicProvider.class); 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 final String JSON_MUSIC = "music"; private static final String JSON_TITLE = "title"; private static final String JSON_ALBUM = "album"; private static final String JSON_ARTIST = "artist"; private static final String JSON_GENRE = "genre"; private static final String JSON_SOURCE = "source"; private static final String JSON_IMAGE = "image"; private static final String JSON_TRACK_NUMBER = "trackNumber"; private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount"; private static final String JSON_DURATION = "duration"; private static final Uri ART_CONTENT_URI = Uri.parse("content://media/external/audio/albumart"); // Categorized caches for music track data: private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByGenre; private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByArtist; private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByAlbum; private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById; private final Set<String> mFavoriteTracks; enum State { NON_INITIALIZED, INITIALIZING, INITIALIZED } private volatile State mCurrentState = State.NON_INITIALIZED; public interface Callback { void onMusicCatalogReady(boolean success); } public MusicProvider() { mMusicListByGenre = new ConcurrentHashMap<>(); mMusicListByArtist = new ConcurrentHashMap<>(); mMusicListByAlbum = new ConcurrentHashMap<>(); mMusicListById = new ConcurrentHashMap<>(); mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); } /** * Get an iterator over the list of genres * * @return genres */ public Iterable<String> getGenres() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } return mMusicListByGenre.keySet(); } /** * Get an iterator over the list of albums * * @return albums */ public Iterable<String> getAlbums() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } return mMusicListByAlbum.keySet(); } public String getAlbumArt(String album) { if (mCurrentState != State.INITIALIZED) { return ""; } List<MediaMetadataCompat> list = mMusicListByAlbum.get(album); if (list == null || list.size() == 0) { return ""; } return list.get(0).getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI); } /** * Get an iterator over the list of artists * * @return artists */ public Iterable<String> getArtists() { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } return mMusicListByArtist.keySet(); } /** * Get music tracks of the given genre */ public Iterable<MediaMetadataCompat> getMusicsByGenre(String genre) { if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) { return Collections.emptyList(); } return mMusicListByGenre.get(genre); } /** * Get music tracks of the given genre */ public Iterable<MediaMetadataCompat> getMusicsByArtist(String artist) { if (mCurrentState != State.INITIALIZED || !mMusicListByArtist.containsKey(artist)) { return Collections.emptyList(); } return mMusicListByArtist.get(artist); } /** * Get music tracks of the given genre */ public Iterable<MediaMetadataCompat> getMusicsByAlbum(String album) { if (mCurrentState != State.INITIALIZED || !mMusicListByAlbum.containsKey(album)) { return Collections.emptyList(); } return mMusicListByAlbum.get(album); } /** * Very basic implementation of a search that filter music tracks with title containing * the given query. */ public Iterable<MediaMetadataCompat> searchMusicBySongTitle(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_TITLE, query); } /** * Very basic implementation of a search that filter music tracks with album containing * the given query. */ public Iterable<MediaMetadataCompat> searchMusicByAlbum(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_ALBUM, query); } /** * Very basic implementation of a search that filter music tracks with artist containing * the given query. */ public Iterable<MediaMetadataCompat> searchMusicByArtist(String query) { return searchMusic(MediaMetadataCompat.METADATA_KEY_ARTIST, query); } Iterable<MediaMetadataCompat> searchMusic(String metadataField, String query) { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } ArrayList<MediaMetadataCompat> result = new ArrayList<>(); query = query.toLowerCase(Locale.US); for (MutableMediaMetadata track : mMusicListById.values()) { if (track.metadata.getString(metadataField).toLowerCase(Locale.US).contains(query)) { result.add(track.metadata); } } return result; } /** * Return the MediaMetadata for the given musicID. * * @param musicId The unique, non-hierarchical music ID. */ public MediaMetadataCompat getMusic(String musicId) { return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null; } public synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) { MutableMediaMetadata track = mMusicListById.get(musicId); if (track == null) { return; } String oldGenre = track.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE); String newGenre = metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE); track.metadata = metadata; // if genre has changed, we need to rebuild the list by genre if (!oldGenre.equals(newGenre)) { buildListsByGenre(); } } public void setFavorite(String musicId, boolean favorite) { if (favorite) { mFavoriteTracks.add(musicId); } else { mFavoriteTracks.remove(musicId); } } 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 musicId and grouping by genre. */ public void retrieveMediaAsync(final Callback callback) { LogUtils.d(TAG, "retrieveMediaAsync called"); 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<Void, Void, State>() { @Override protected State doInBackground(Void... params) { retrieveMedia(); return mCurrentState; } @Override protected void onPostExecute(State current) { if (callback != null) { callback.onMusicCatalogReady(current == State.INITIALIZED); } } }.execute(); } private synchronized void buildListsByGenre() { ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByGenre = new ConcurrentHashMap<>(); for (MutableMediaMetadata m : mMusicListById.values()) { String genre = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE); List<MediaMetadataCompat> list = newMusicListByGenre.get(genre); if (list == null) { list = new ArrayList<>(); newMusicListByGenre.put(genre, list); } list.add(m.metadata); } mMusicListByGenre = newMusicListByGenre; } private synchronized void buildListsByAlbum() { ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByAlbum = new ConcurrentHashMap<>(); for (MutableMediaMetadata m : mMusicListById.values()) { String album = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); List<MediaMetadataCompat> list = newMusicListByAlbum.get(album); if (list == null) { list = new ArrayList<>(); newMusicListByAlbum.put(album, list); } list.add(m.metadata); } mMusicListByAlbum = newMusicListByAlbum; } private synchronized void buildListsByArtist() { ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByArtist = new ConcurrentHashMap<>(); for (MutableMediaMetadata m : mMusicListById.values()) { String artist = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST); List<MediaMetadataCompat> list = newMusicListByArtist.get(artist); if (list == null) { list = new ArrayList<>(); newMusicListByArtist.put(artist, list); } list.add(m.metadata); } mMusicListByArtist = newMusicListByArtist; } private static final String UNKNOWN_TAG = LHApplication.getResource().getString(R.string.tag_not_found); private synchronized void retrieveMedia() { try { if (mCurrentState == State.NON_INITIALIZED) { mCurrentState = State.INITIALIZING; int index; long genreId; Uri uri; Cursor genreMediaCursor = null; String[] projection1 = {MediaStore.Audio.Genres.NAME, MediaStore.Audio.Genres._ID}; String[] projection2 = { MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, MediaStore.Audio.Media.ALBUM, MediaStore.Audio.Media.DURATION, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TRACK, MediaStore.Audio.Media.SIZE, MediaStore.Audio.Media._ID, MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DISPLAY_NAME}; String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"; Cursor genreCursor = LHApplication.getContext().getContentResolver().query( MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, projection1, null, null, null); if (genreCursor.moveToFirst()) { do { String genre = genreCursor.getString( genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)); if (genre != null && genre.trim().equals("")) { genre = UNKNOWN_TAG; } index = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID); genreId = Long.parseLong(genreCursor.getString(index)); uri = MediaStore.Audio.Genres.Members.getContentUri("external", genreId); genreMediaCursor = LHApplication.getContext().getContentResolver().query(uri, projection2, selection, null, null); if (genreMediaCursor.moveToFirst()) { do { String title = genreMediaCursor.getString(0); String artist = genreMediaCursor.getString(1); String album = genreMediaCursor.getString(2); long duration = genreMediaCursor.getLong(3); String source = genreMediaCursor.getString(4); int trackNumber = genreMediaCursor.getInt(5); long totalTrackCount = genreMediaCursor.getLong(6); String musicId = genreMediaCursor.getString(7); long albumId = genreMediaCursor.getLong(8); MediaMetadataCompat item = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, musicId) .putString(CUSTOM_METADATA_TRACK_SOURCE, source) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration) .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, MusicUtils.getAlbumArtworkUri(albumId).toString()) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount) .build(); mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item)); } while(genreMediaCursor.moveToNext()); } } while(genreCursor.moveToNext()); } genreCursor.close(); if (genreMediaCursor != null) { genreMediaCursor.close(); } Cursor allSongCursor = MusicUtils.getAllSongsCursor(LHApplication.getContext()); try { if (allSongCursor.moveToFirst()) { do { String title = allSongCursor.getString(0); String artist = allSongCursor.getString(1); String album = allSongCursor.getString(2); long duration = allSongCursor.getLong(3); String source = allSongCursor.getString(4); int trackNumber = allSongCursor.getInt(5); long totalTrackCount = allSongCursor.getLong(6); String musicId = allSongCursor.getString(7); long albumId = allSongCursor.getLong(8); if (mMusicListById.containsKey(musicId)) { continue; } MediaMetadataCompat item = new MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, musicId) .putString(CUSTOM_METADATA_TRACK_SOURCE, source) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration) .putString(MediaMetadataCompat.METADATA_KEY_GENRE, UNKNOWN_TAG) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, MusicUtils.getAlbumArtworkUri(albumId).toString()) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount) .build(); mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item)); } while(allSongCursor.moveToNext()); } } finally { if (allSongCursor != null) { allSongCursor.close(); } } buildListsByGenre(); buildListsByArtist(); buildListsByAlbum(); mCurrentState = State.INITIALIZED; } } 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; } } } private synchronized void retrieveMediaFromGoogle() { try { if (mCurrentState == State.NON_INITIALIZED) { mCurrentState = State.INITIALIZING; int slashPos = CATALOG_URL.lastIndexOf('/'); String path = CATALOG_URL.substring(0, slashPos + 1); JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL); if (jsonObj == null) { return; } JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); if (tracks != null) { for (int j = 0; j < tracks.length(); j++) { MediaMetadataCompat item = buildFromJSON(tracks.getJSONObject(j), path); String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID); mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item)); } buildListsByGenre(); } mCurrentState = State.INITIALIZED; } } catch (JSONException e) { LogUtils.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; } } } private MediaMetadataCompat 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 LogUtils.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 MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) .putString(CUSTOM_METADATA_TRACK_SOURCE, source) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist) .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration) .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber) .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount) .build(); } /** * Download a JSON file from a server, parse the content and return the JSON object. * * @return result JSONObject containing the parsed representation. */ private JSONObject fetchJSONFromUrl(String urlString) { BufferedReader reader = null; try { URLConnection urlConnection = new URL(urlString).openConnection(); reader = new BufferedReader(new InputStreamReader( urlConnection.getInputStream(), "iso-8859-1")); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return new JSONObject(sb.toString()); } catch (Exception e) { LogUtils.e(TAG, "Failed to parse the json for media list", e); return null; } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { // ignore } } } } }