Home | History | Annotate | Download | only in model
      1 /*
      2  * Copyright (C) 2014 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.mediabrowserservice.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.Collections;
     35 import java.util.LinkedHashMap;
     36 
     37 /**
     38  * Utility class to get a list of MusicTrack's based on a server-side JSON
     39  * configuration.
     40  *
     41  * In a real application this class may pull data from a remote server, as we do here,
     42  * or potentially use {@link android.provider.MediaStore} to locate media files located on
     43  * the device.
     44  */
     45 public class MusicProvider {
     46 
     47     private static final String TAG = MusicProvider.class.getSimpleName();
     48 
     49     public static final String MEDIA_ID_ROOT = "__ROOT__";
     50     public static final String MEDIA_ID_EMPTY_ROOT = "__EMPTY__";
     51 
     52     private static final String CATALOG_URL =
     53             "https://storage.googleapis.com/automotive-media/music.json";
     54 
     55     private static final String JSON_MUSIC = "music";
     56     private static final String JSON_TITLE = "title";
     57     private static final String JSON_ALBUM = "album";
     58     private static final String JSON_ARTIST = "artist";
     59     private static final String JSON_GENRE = "genre";
     60     private static final String JSON_SOURCE = "source";
     61     private static final String JSON_IMAGE = "image";
     62     private static final String JSON_TRACK_NUMBER = "trackNumber";
     63     private static final String JSON_TOTAL_TRACK_COUNT = "totalTrackCount";
     64     private static final String JSON_DURATION = "duration";
     65 
     66     // Categorized caches for music track data:
     67     private final LinkedHashMap<String, MediaMetadataCompat> mMusicListById;
     68 
     69     private enum State {
     70         NON_INITIALIZED, INITIALIZING, INITIALIZED
     71     }
     72 
     73     private volatile State mCurrentState = State.NON_INITIALIZED;
     74 
     75     /**
     76      * Callback used by MusicService.
     77      */
     78     public interface Callback {
     79         void onMusicCatalogReady(boolean success);
     80     }
     81 
     82     public MusicProvider() {
     83         mMusicListById = new LinkedHashMap<>();
     84     }
     85 
     86     public Iterable<MediaMetadataCompat> getAllMusics() {
     87         if (mCurrentState != State.INITIALIZED || mMusicListById.isEmpty()) {
     88             return Collections.emptyList();
     89         }
     90         return mMusicListById.values();
     91     }
     92 
     93     /**
     94      * Return the MediaMetadata for the given musicID.
     95      *
     96      * @param musicId The unique music ID.
     97      */
     98     public MediaMetadataCompat getMusic(String musicId) {
     99         return mMusicListById.containsKey(musicId) ? mMusicListById.get(musicId) : null;
    100     }
    101 
    102     /**
    103      * Update the metadata associated with a musicId. If the musicId doesn't exist, the
    104      * update is dropped. (That is, it does not create a new mediaId.)
    105      * @param musicId The ID
    106      * @param metadata New Metadata to associate with it
    107      */
    108     public synchronized void updateMusic(String musicId, MediaMetadataCompat metadata) {
    109         MediaMetadataCompat track = mMusicListById.get(musicId);
    110         if (track != null) {
    111             mMusicListById.put(musicId, metadata);
    112         }
    113     }
    114 
    115     public boolean isInitialized() {
    116         return mCurrentState == State.INITIALIZED;
    117     }
    118 
    119     /**
    120      * Get the list of music tracks from a server and caches the track information
    121      * for future reference, keying tracks by musicId and grouping by genre.
    122      */
    123     public void retrieveMediaAsync(final Callback callback) {
    124         Log.d(TAG, "retrieveMediaAsync called");
    125         if (mCurrentState == State.INITIALIZED) {
    126             // Already initialized, so call back immediately.
    127             callback.onMusicCatalogReady(true);
    128             return;
    129         }
    130 
    131         // Asynchronously load the music catalog in a separate thread
    132         new AsyncTask<Void, Void, State>() {
    133             @Override
    134             protected State doInBackground(Void... params) {
    135                 retrieveMedia();
    136                 return mCurrentState;
    137             }
    138 
    139             @Override
    140             protected void onPostExecute(State current) {
    141                 if (callback != null) {
    142                     callback.onMusicCatalogReady(current == State.INITIALIZED);
    143                 }
    144             }
    145         }.execute();
    146     }
    147 
    148     private synchronized void retrieveMedia() {
    149         try {
    150             if (mCurrentState == State.NON_INITIALIZED) {
    151                 mCurrentState = State.INITIALIZING;
    152 
    153                 int slashPos = CATALOG_URL.lastIndexOf('/');
    154                 String path = CATALOG_URL.substring(0, slashPos + 1);
    155                 JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL);
    156                 if (jsonObj == null) {
    157                     return;
    158                 }
    159                 JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC);
    160                 if (tracks != null) {
    161                     for (int j = tracks.length() - 1; j >= 0; j--) {
    162                         MediaMetadataCompat item = buildFromJSON(tracks.getJSONObject(j), path);
    163                         String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID);
    164                         mMusicListById.put(musicId, item);
    165                     }
    166                 }
    167                 mCurrentState = State.INITIALIZED;
    168             }
    169         } catch (JSONException jsonException) {
    170             Log.e(TAG, "Could not retrieve music list", jsonException);
    171         } finally {
    172             if (mCurrentState != State.INITIALIZED) {
    173                 // Something bad happened, so we reset state to NON_INITIALIZED to allow
    174                 // retries (eg if the network connection is temporary unavailable)
    175                 mCurrentState = State.NON_INITIALIZED;
    176             }
    177         }
    178     }
    179 
    180     private MediaMetadataCompat buildFromJSON(JSONObject json, String basePath)
    181             throws JSONException {
    182 
    183         String title = json.getString(JSON_TITLE);
    184         String album = json.getString(JSON_ALBUM);
    185         String artist = json.getString(JSON_ARTIST);
    186         String genre = json.getString(JSON_GENRE);
    187         String source = json.getString(JSON_SOURCE);
    188         String iconUrl = json.getString(JSON_IMAGE);
    189         int trackNumber = json.getInt(JSON_TRACK_NUMBER);
    190         int totalTrackCount = json.getInt(JSON_TOTAL_TRACK_COUNT);
    191         int duration = json.getInt(JSON_DURATION) * 1000; // ms
    192 
    193         Log.d(TAG, "Found music track: " + json);
    194 
    195         // Media is stored relative to JSON file
    196         if (!source.startsWith("https")) {
    197             source = basePath + source;
    198         }
    199         if (!iconUrl.startsWith("https")) {
    200             iconUrl = basePath + iconUrl;
    201         }
    202         // Since we don't have a unique ID in the server, we fake one using the hashcode of
    203         // the music source. In a real world app, this could come from the server.
    204         String id = String.valueOf(source.hashCode());
    205 
    206         // Adding the music source to the MediaMetadata (and consequently using it in the
    207         // mediaSession.setMetadata) is not a good idea for a real world music app, because
    208         // the session metadata can be accessed by notification listeners. This is done in this
    209         // sample for convenience only.
    210         return new MediaMetadataCompat.Builder()
    211                 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
    212                 .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, source)
    213                 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
    214                 .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
    215                 .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration)
    216                 .putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre)
    217                 .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, iconUrl)
    218                 .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
    219                 .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, trackNumber)
    220                 .putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, totalTrackCount)
    221                 .build();
    222     }
    223 
    224     /**
    225      * Download a JSON file from a server, parse the content and return the JSON
    226      * object.
    227      *
    228      * @return result JSONObject containing the parsed representation.
    229      */
    230     private JSONObject fetchJSONFromUrl(String urlString) {
    231         InputStream inputStream = null;
    232         try {
    233             URL url = new URL(urlString);
    234             URLConnection urlConnection = url.openConnection();
    235             inputStream = new BufferedInputStream(urlConnection.getInputStream());
    236             BufferedReader reader = new BufferedReader(new InputStreamReader(
    237                     urlConnection.getInputStream(), "iso-8859-1"));
    238             StringBuilder stringBuilder = new StringBuilder();
    239             String line;
    240             while ((line = reader.readLine()) != null) {
    241                 stringBuilder.append(line);
    242             }
    243             return new JSONObject(stringBuilder.toString());
    244         } catch (IOException | JSONException exception) {
    245             Log.e(TAG, "Failed to parse the json for media list", exception);
    246             return null;
    247         } finally {
    248             // If the inputStream was opened, try to close it now.
    249             if (inputStream != null) {
    250                 try {
    251                     inputStream.close();
    252                 } catch (IOException ignored) {
    253                     // Ignore the exception since there's nothing left to do with the stream
    254                 }
    255             }
    256         }
    257     }
    258 }
    259