Home | History | Annotate | Download | only in model
      1 /*
      2  * Copyright (C) 2015 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.supportv4.media.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.ArrayList;
     35 import java.util.Collections;
     36 import java.util.List;
     37 import java.util.Set;
     38 import java.util.concurrent.ConcurrentHashMap;
     39 import java.util.concurrent.ConcurrentMap;
     40 
     41 /**
     42  * Utility class to get a list of MusicTrack's based on a server-side JSON
     43  * configuration.
     44  */
     45 public class MusicProvider {
     46 
     47     private static final String TAG = "MusicProvider";
     48 
     49     private static final String CATALOG_URL =
     50         "http://storage.googleapis.com/automotive-media/music.json";
     51 
     52     public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
     53 
     54     private static final String JSON_MUSIC = "music";
     55     private static final String JSON_TITLE = "title";
     56     private static final String JSON_ALBUM = "album";
     57     private static final String JSON_ARTIST = "artist";
     58     private static final String JSON_GENRE = "genre";
     59     private static final String JSON_SOURCE = "source";
     60     private static final String JSON_IMAGE = "image";
     61     private static final String JSON_TRACK_NUMBER = "trackNumber";
     62     private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
     63     private static final String JSON_DURATION = "duration";
     64 
     65     // Categorized caches for music track data:
     66     private ConcurrentMap<String, List<MediaMetadataCompat>> mMusicListByGenre;
     67     private List<String> mMusicGenres;
     68     private final ConcurrentMap<String, MutableMediaMetadata> mMusicListById;
     69 
     70     private final Set<String> mFavoriteTracks;
     71 
     72     enum State {
     73         NON_INITIALIZED, INITIALIZING, INITIALIZED
     74     }
     75 
     76     private volatile State mCurrentState = State.NON_INITIALIZED;
     77 
     78     public interface Callback {
     79         void onMusicCatalogReady(boolean success);
     80     }
     81 
     82     public MusicProvider() {
     83         mMusicListByGenre = new ConcurrentHashMap<>();
     84         mMusicListById = new ConcurrentHashMap<>();
     85         mFavoriteTracks = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
     86         mMusicGenres = new ArrayList<>();
     87     }
     88 
     89     /**
     90      * Get the list of genres
     91      *
     92      * @return genres
     93      */
     94     public List<String> getGenres() {
     95         if (mCurrentState != State.INITIALIZED) {
     96             return Collections.emptyList();
     97         }
     98         return mMusicGenres;
     99     }
    100 
    101     /**
    102      * Get music tracks of the given genre
    103      */
    104     public List<MediaMetadataCompat> getMusicsByGenre(String genre) {
    105         if (mCurrentState != State.INITIALIZED || !mMusicListByGenre.containsKey(genre)) {
    106             return Collections.emptyList();
    107         }
    108         return mMusicListByGenre.get(genre);
    109     }
    110 
    111     /**
    112      * Very basic implementation of a search that filter music tracks which title containing
    113      * the given query.
    114      *
    115      */
    116     public Iterable<MediaMetadataCompat> searchMusic(String titleQuery) {
    117         if (mCurrentState != State.INITIALIZED) {
    118             return Collections.emptyList();
    119         }
    120         ArrayList<MediaMetadataCompat> result = new ArrayList<>();
    121         titleQuery = titleQuery.toLowerCase();
    122         for (MutableMediaMetadata track : mMusicListById.values()) {
    123             if (track.metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE).toLowerCase()
    124                     .contains(titleQuery)) {
    125                 result.add(track.metadata);
    126             }
    127         }
    128         return result;
    129     }
    130 
    131     /**
    132      * Return the MediaMetadata for the given musicID.
    133      *
    134      * @param musicId The unique, non-hierarchical music ID.
    135      */
    136     public MediaMetadataCompat getMusic(String musicId) {
    137         return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId).metadata : null;
    138     }
    139 
    140     public synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) {
    141         MutableMediaMetadata track = mMusicListById.get(musicId);
    142         if (track == null) {
    143             return;
    144         }
    145 
    146         String oldGenre = track.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE);
    147         String newGenre = metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE);
    148 
    149         track.metadata = metadata;
    150 
    151         // if genre has changed, we need to rebuild the list by genre
    152         if (!oldGenre.equals(newGenre)) {
    153             buildListsByGenre();
    154         }
    155     }
    156 
    157     public void setFavorite(String musicId, boolean favorite) {
    158         if (favorite) {
    159             mFavoriteTracks.add(musicId);
    160         } else {
    161             mFavoriteTracks.remove(musicId);
    162         }
    163     }
    164 
    165     public boolean isFavorite(String musicId) {
    166         return mFavoriteTracks.contains(musicId);
    167     }
    168 
    169     public boolean isInitialized() {
    170         return mCurrentState == State.INITIALIZED;
    171     }
    172 
    173     /**
    174      * Get the list of music tracks from a server and caches the track information
    175      * for future reference, keying tracks by musicId and grouping by genre.
    176      */
    177     public void retrieveMediaAsync(final Callback callback) {
    178         Log.d(TAG, "retrieveMediaAsync called");
    179         if (mCurrentState == State.INITIALIZED) {
    180             // Nothing to do, execute callback immediately
    181             callback.onMusicCatalogReady(true);
    182             return;
    183         }
    184 
    185         // Asynchronously load the music catalog in a separate thread
    186         new AsyncTask<Void, Void, State>() {
    187             @Override
    188             protected State doInBackground(Void... params) {
    189                 retrieveMedia();
    190                 return mCurrentState;
    191             }
    192 
    193             @Override
    194             protected void onPostExecute(State current) {
    195                 if (callback != null) {
    196                     callback.onMusicCatalogReady(current == State.INITIALIZED);
    197                 }
    198             }
    199         }.execute();
    200     }
    201 
    202     private synchronized void buildListsByGenre() {
    203         ConcurrentMap<String, List<MediaMetadataCompat>> newMusicListByGenre
    204                 = new ConcurrentHashMap<>();
    205 
    206         for (MutableMediaMetadata m : mMusicListById.values()) {
    207             String genre = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE);
    208             List<MediaMetadataCompat> list = newMusicListByGenre.get(genre);
    209             if (list == null) {
    210                 list = new ArrayList<>();
    211                 newMusicListByGenre.put(genre, list);
    212             }
    213             list.add(m.metadata);
    214         }
    215         mMusicListByGenre = newMusicListByGenre;
    216         mMusicGenres = new ArrayList<>(mMusicListByGenre.keySet());
    217     }
    218 
    219     private synchronized void retrieveMedia() {
    220         try {
    221             if (mCurrentState == State.NON_INITIALIZED) {
    222                 mCurrentState = State.INITIALIZING;
    223 
    224                 int slashPos = CATALOG_URL.lastIndexOf('/');
    225                 String path = CATALOG_URL.substring(0, slashPos + 1);
    226                 JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);
    227                 if (jsonObj == null) {
    228                     return;
    229                 }
    230                 JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
    231                 if (tracks != null) {
    232                     for (int j = 0; j < tracks.length(); j++) {
    233                         MediaMetadataCompat item = buildFromJSON(tracks.getJSONObject(j), path);
    234                         String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
    235                         mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item));
    236                     }
    237                     buildListsByGenre();
    238                 }
    239                 mCurrentState = State.INITIALIZED;
    240             }
    241         } catch (JSONException e) {
    242             Log.e(TAG, "Could not retrieve music list", e);
    243         } finally {
    244             if (mCurrentState != State.INITIALIZED) {
    245                 // Something bad happened, so we reset state to NON_INITIALIZED to allow
    246                 // retries (eg if the network connection is temporary unavailable)
    247                 mCurrentState = State.NON_INITIALIZED;
    248             }
    249         }
    250     }
    251 
    252     private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath) throws JSONException {
    253         String title = json.getString(JSON_TITLE);
    254         String album = json.getString(JSON_ALBUM);
    255         String artist = json.getString(JSON_ARTIST);
    256         String genre = json.getString(JSON_GENRE);
    257         String source = json.getString(JSON_SOURCE);
    258         String iconUrl = json.getString(JSON_IMAGE);
    259         int trackNumber = json.getInt(JSON_TRACK_NUMBER);
    260         int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
    261         int duration = json.getInt(JSON_DURATION) * 1000; // ms
    262 
    263         Log.d(TAG, "Found music track: " + json);
    264 
    265         // Media is stored relative to JSON file
    266         if (!source.startsWith("http")) {
    267             source = basePath + source;
    268         }
    269         if (!iconUrl.startsWith("http")) {
    270             iconUrl = basePath + iconUrl;
    271         }
    272         // Since we don't have a unique ID in the server, we fake one using the hashcode of
    273         // the music source. In a real world app, this could come from the server.
    274         String id = String.valueOf(source.hashCode());
    275 
    276         // Adding the music source to the MediaMetadata (and consequently using it in the
    277         // mediaSession.setMetadata) is not a good idea for a real world music app, because
    278         // the session metadata can be accessed by notification listeners. This is done in this
    279         // sample for convenience only.
    280         return new MediaMetadataCompat.Builder()
    281                 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
    282                 .putString(CUSTOM_METADATA_TRACK_SOURCE, source)
    283                 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
    284                 .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
    285                 .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
    286                 .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
    287                 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)
    288                 .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
    289                 .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
    290                 .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)
    291                 .build();
    292     }
    293 
    294     /**
    295      * Download a JSON file from a server, parse the content and return the JSON
    296      * object.
    297      *
    298      * @return result JSONObject containing the parsed representation.
    299      */
    300     private JSONObject fetchJSONFromUrl(String urlString) {
    301         InputStream is = null;
    302         try {
    303             URL url = new URL(urlString);
    304             URLConnection urlConnection = url.openConnection();
    305             is = new BufferedInputStream(urlConnection.getInputStream());
    306             BufferedReader reader = new BufferedReader(new InputStreamReader(
    307                     urlConnection.getInputStream(), "iso-8859-1"));
    308             StringBuilder sb = new StringBuilder();
    309             String line;
    310             while ((line = reader.readLine()) != null) {
    311                 sb.append(line);
    312             }
    313             return new JSONObject(sb.toString());
    314         } catch (Exception e) {
    315             Log.e(TAG, "Failed to parse the json for media list", e);
    316             return null;
    317         } finally {
    318             if (is != null) {
    319                 try {
    320                     is.close();
    321                 } catch (IOException e) {
    322                     // ignore
    323                 }
    324             }
    325         }
    326     }
    327 }
    328