com.scooter1556.sms.android.service.MediaService.java Source code

Java tutorial

Introduction

Here is the source code for com.scooter1556.sms.android.service.MediaService.java

Source

/*
 * Author: Scott Ware <scoot.software@gmail.com>
 * Copyright (c) 2015 Scott Ware
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.scooter1556.sms.android.service;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.media.MediaRouter;
import android.util.Log;

import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManager;
import com.google.android.gms.cast.framework.SessionManagerListener;
import com.google.gson.Gson;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.scooter1556.sms.android.R;
import com.scooter1556.sms.android.activity.NowPlayingActivity;
import com.scooter1556.sms.android.domain.Playlist;
import com.scooter1556.sms.android.manager.MediaNotificationManager;
import com.scooter1556.sms.android.playback.AudioPlayback;
import com.scooter1556.sms.android.playback.CastPlayback;
import com.scooter1556.sms.android.playback.Playback;
import com.scooter1556.sms.android.playback.PlaybackManager;
import com.scooter1556.sms.android.playback.QueueManager;
import com.scooter1556.sms.android.utils.AutoUtils;
import com.scooter1556.sms.android.utils.NetworkUtils;
import com.scooter1556.sms.android.utils.ResourceUtils;
import com.scooter1556.sms.android.utils.TVUtils;
import com.scooter1556.sms.android.database.ConnectionDatabase;
import com.scooter1556.sms.android.domain.Connection;
import com.scooter1556.sms.android.domain.MediaElement;
import com.scooter1556.sms.android.domain.MediaFolder;
import com.scooter1556.sms.android.utils.MediaUtils;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class MediaService extends MediaBrowserServiceCompat implements PlaybackManager.PlaybackServiceCallback {

    private static final String TAG = "MediaService";

    // Extra on MediaSession that contains the Cast device name currently connected to
    public static final String EXTRA_CONNECTED_CAST = "com.example.android.uamp.CAST_NAME";
    // The action of the incoming Intent indicating that it contains a command
    // to be executed (see {@link #onStartCommand})
    public static final String ACTION_CMD = "com.example.android.uamp.ACTION_CMD";
    // The key in the extras of the incoming Intent indicating the command that
    // should be executed (see {@link #onStartCommand})
    public static final String CMD_NAME = "CMD_NAME";
    // A value of a CMD_NAME key in the extras of the incoming Intent that
    // indicates that the music playback should be paused (see {@link #onStartCommand})
    public static final String CMD_PAUSE = "CMD_PAUSE";
    // A value of a CMD_NAME key that indicates that the music playback should switch
    // to local playback from cast playback.
    public static final String CMD_STOP_CASTING = "CMD_STOP_CASTING";

    // Shuffle
    public static final String STATE_SHUFFLE_ON = "state_shuffle_on";
    public static final String STATE_SHUFFLE_OFF = "state_shuffle_off";

    // Repeat
    public static final String STATE_REPEAT_NONE = "state_repeat_none";
    public static final String STATE_REPEAT_ALL = "state_repeat_all";
    public static final String STATE_REPEAT_ONE = "state_repeat_one";

    // Actions
    public static final String ACTION_CLEAR_PLAYLIST = "action_clear_playlist";

    // Delay stopSelf by using a handler.
    private static final int STOP_DELAY = 30000;

    // Number of media elements to fetch when populating lists
    public static final int FETCH_LIMIT = 50;

    private PlaybackManager playbackManager;
    private QueueManager queueManager;

    private MediaSessionCompat mediaSession;
    private Bundle mediaSessionExtras;
    private MediaNotificationManager mediaNotificationManager;
    private final DelayedStopHandler delayedStopHandler = new DelayedStopHandler(this);
    private MediaRouter mediaRouter;
    private SessionManager castSessionManager;
    private SessionManagerListener<CastSession> castSessionManagerListener;

    boolean isOnline = false;
    private boolean isConnected = false;
    private boolean isConnectedToCar;
    private BroadcastReceiver carConnectionReceiver;

    // REST Client
    RESTService restService = null;

    private final BroadcastReceiver connectivityChangeReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            boolean status = NetworkUtils.isOnline(context);

            if (isOnline != status) {
                isOnline = status;
            }
        }
    };

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onCreate() {
        super.onCreate();

        Log.d(TAG, "onCreate()");

        restService = RESTService.getInstance();

        // Retrieve preferences if they exist
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

        // Load default settings
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

        // Initialise database
        ConnectionDatabase db = new ConnectionDatabase(this);

        // Check connection
        long id = sharedPreferences.getLong("Connection", -1);

        if (id >= 0) {
            Connection connection = db.getConnection(id);
            restService.setConnection(connection);
            isConnected = true;
        } else {
            isConnected = false;
        }

        queueManager = new QueueManager(getApplicationContext(), new QueueManager.MetadataUpdateListener() {
            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) {
                Log.d(TAG, "onMetadataChanged()");

                mediaSession.setMetadata(metadata);
            }

            @Override
            public void onMetadataRetrieveError() {
                Log.d(TAG, "onMetadataRetrieveError()");

                playbackManager.updatePlaybackState(getString(R.string.error_no_metadata));
            }

            @Override
            public void onCurrentQueueIndexUpdated(int queueIndex) {
                Log.d(TAG, "onCurrentQueueIndexUpdated(" + queueIndex + ")");

                playbackManager.handlePlayRequest();
            }

            @Override
            public void onQueueUpdated(List<MediaSessionCompat.QueueItem> newQueue) {
                Log.d(TAG, "onQueueUpdated()");

                mediaSession.setQueue(newQueue);
                mediaSession.setQueueTitle("Now Playing");
            }
        });

        // Initialise playback manager
        playbackManager = PlaybackManager.getInstance();
        playbackManager.initialise(getApplicationContext(), this, queueManager);

        // Start a new Media Session
        mediaSession = new MediaSessionCompat(this, MediaService.class.getSimpleName());
        mediaSession.setCallback(playbackManager.getMediaSessionCallback());
        mediaSession.setFlags(
                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
                        | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);

        Context context = getApplicationContext();
        Intent intent = new Intent(context, NowPlayingActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 99, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        mediaSession.setSessionActivity(pendingIntent);

        mediaSessionExtras = new Bundle();
        AutoUtils.setSlotReservationFlags(mediaSessionExtras, true, true, true);
        mediaSession.setExtras(mediaSessionExtras);

        setSessionToken(mediaSession.getSessionToken());

        try {
            mediaNotificationManager = new MediaNotificationManager(this);
        } catch (RemoteException e) {
            throw new IllegalStateException("Could not create a MediaNotificationManager", e);
        }

        if (!TVUtils.isTvUiMode(this)) {
            castSessionManager = CastContext.getSharedInstance(this).getSessionManager();
            castSessionManagerListener = new CastSessionManagerListener();
            castSessionManager.addSessionManagerListener(castSessionManagerListener, CastSession.class);
        }

        mediaRouter = MediaRouter.getInstance(getApplicationContext());

        registerCarConnectionReceiver();

        // Register connectivity receiver
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        this.registerReceiver(connectivityChangeReceiver, intentFilter);
    }

    @Override
    public int onStartCommand(Intent startIntent, int flags, int startId) {
        Log.d(TAG, "onStartCommand()");

        if (startIntent != null) {
            String action = startIntent.getAction();
            String command = startIntent.getStringExtra(CMD_NAME);

            if (ACTION_CMD.equals(action)) {
                if (CMD_PAUSE.equals(command)) {
                    playbackManager.handlePauseRequest();
                } else if (CMD_STOP_CASTING.equals(command)) {
                    CastContext.getSharedInstance(this).getSessionManager().endCurrentSession(true);
                }
            } else {
                // Try to handle the intent as a media button event wrapped by MediaButtonReceiver
                MediaButtonReceiver.handleIntent(mediaSession, startIntent);
            }
        }

        // Reset the delay handler to enqueue a message to stop the service if
        // nothing is playing.
        delayedStopHandler.removeCallbacksAndMessages(null);
        delayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);

        return START_STICKY;
    }

    /*
     * Handle case when user swipes the app away from the recent apps list by
     * stopping the service (and any ongoing playback).
     */
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        super.onTaskRemoved(rootIntent);
        stopSelf();
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy()");

        unregisterCarConnectionReceiver();

        // Service is being killed, so make sure we release our resources
        playbackManager.handleStopRequest(null);
        mediaNotificationManager.stopNotification();

        if (castSessionManager != null) {
            castSessionManager.removeSessionManagerListener(castSessionManagerListener, CastSession.class);
        }

        delayedStopHandler.removeCallbacksAndMessages(null);
        mediaSession.release();

        // Unregister receiver
        this.unregisterReceiver(connectivityChangeReceiver);
    }

    /**
     * Callback method called from PlaybackManager whenever the music is about to play.
     */
    @Override
    public void onPlaybackStart() {
        Log.d(TAG, "onPlaybackStart()");

        mediaSession.setActive(true);

        delayedStopHandler.removeCallbacksAndMessages(null);

        // The service needs to continue running even after the bound client (usually a
        // MediaController) disconnects, otherwise the media playback will stop.
        // Calling startService(Intent) will keep the service running until it is explicitly killed.
        startService(new Intent(getApplicationContext(), MediaService.class));
    }

    /**
     * Callback method called from PlaybackManager whenever the music stops playing.
     */
    @Override
    public void onPlaybackStop() {
        Log.d(TAG, "onPlaybackStop()");

        mediaSession.setActive(false);

        // Reset the delayed stop handler, so after STOP_DELAY it will be executed again,
        // potentially stopping the service.
        delayedStopHandler.removeCallbacksAndMessages(null);
        delayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
        stopForeground(true);
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void onNotificationRequired() {
        mediaNotificationManager.startNotification();
    }

    @Override
    public void onPlaybackStateUpdated(PlaybackStateCompat newState) {
        mediaSession.setPlaybackState(newState);
    }

    private void registerCarConnectionReceiver() {
        IntentFilter filter = new IntentFilter(AutoUtils.ACTION_MEDIA_STATUS);

        carConnectionReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String connectionEvent = intent.getStringExtra(AutoUtils.MEDIA_CONNECTION_STATUS);
                isConnectedToCar = AutoUtils.MEDIA_CONNECTED.equals(connectionEvent);
                Log.i(TAG, "Connection event to Android Auto: " + connectionEvent + " isConnectedToCar="
                        + isConnectedToCar);
            }
        };

        registerReceiver(carConnectionReceiver, filter);
    }

    private void unregisterCarConnectionReceiver() {
        unregisterReceiver(carConnectionReceiver);
    }

    /**
     * A simple handler that stops the service if playback is not active (playing)
     */
    private static class DelayedStopHandler extends Handler {
        private final WeakReference<MediaService> reference;

        private DelayedStopHandler(MediaService service) {
            reference = new WeakReference<>(service);
        }

        @Override
        public void handleMessage(Message msg) {
            MediaService service = reference.get();

            if (service != null && service.playbackManager.getPlayback() != null) {
                if (!service.playbackManager.getPlayback().isPlaying()) {
                    service.stopSelf();
                }
            }
        }
    }

    /**
     * Session Manager Listener responsible for switching the Playback instances
     * depending on whether it is connected to a remote player.
     */
    private class CastSessionManagerListener implements SessionManagerListener<CastSession> {

        @Override
        public void onSessionEnded(CastSession session, int error) {
            Log.d(TAG, "Cast: onSessionEnded()");

            mediaSessionExtras.remove(EXTRA_CONNECTED_CAST);
            mediaSession.setExtras(mediaSessionExtras);
            mediaRouter.setMediaSessionCompat(null);

            Playback playback = new AudioPlayback(MediaService.this);
            playbackManager.switchToPlayback(playback, true);
        }

        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            Log.d(TAG, "Cast: onSessionResumed(" + wasSuspended + ")");

            if (!(playbackManager.getPlayback() instanceof CastPlayback)) {
                // In case we are casting, send the device name as an extra on Media Session metadata.
                mediaSessionExtras.putString(EXTRA_CONNECTED_CAST, session.getCastDevice().getFriendlyName());
                mediaSession.setExtras(mediaSessionExtras);

                // Now we can switch to Cast Playback
                Playback playback = new CastPlayback(MediaService.this);
                mediaRouter.setMediaSessionCompat(mediaSession);
                playbackManager.switchToPlayback(playback, !wasSuspended);
            }
        }

        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            Log.d(TAG, "Cast: onSessionStarted() > sessionId = " + sessionId);

            // Activate session ID on server
            RESTService.getInstance().addSession(UUID.fromString(sessionId));

            // In case we are casting, send the device name as an extra on Media Session metadata.
            mediaSessionExtras.putString(EXTRA_CONNECTED_CAST, session.getCastDevice().getFriendlyName());
            mediaSession.setExtras(mediaSessionExtras);

            // Now we can switch to Cast Playback
            Playback playback = new CastPlayback(MediaService.this);
            mediaRouter.setMediaSessionCompat(mediaSession);
            playbackManager.switchToPlayback(playback, true);
        }

        @Override
        public void onSessionStarting(CastSession session) {
            Log.d(TAG, "Cast: onSessionStarting()");
        }

        @Override
        public void onSessionStartFailed(CastSession session, int error) {
            Log.d(TAG, "Cast: onSessionStartFailed() > error = " + error);
        }

        @Override
        public void onSessionEnding(CastSession session) {
            Log.d(TAG, "Cast: onSessionEnding()");

            // This is our final chance to update the current stream position
            long pos = session.getRemoteMediaClient().getApproximateStreamPosition();
            playbackManager.getPlayback().setCurrentStreamPosition(pos);
        }

        @Override
        public void onSessionResuming(CastSession session, String sessionId) {
            Log.d(TAG, "Cast: onSessionResuming()");
        }

        @Override
        public void onSessionResumeFailed(CastSession session, int error) {
            Log.d(TAG, "Cast: onSessionResumeFailed()");
        }

        @Override
        public void onSessionSuspended(CastSession session, int reason) {
            Log.d(TAG, "Cast: onSessionSuspended()");
        }
    }

    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        Log.d(TAG, "onGetRoot()");

        // Return audio menu if connected to car
        if (isConnectedToCar) {
            return new BrowserRoot(MediaUtils.MEDIA_ID_MENU_AUDIO, null);
        }

        return new BrowserRoot(MediaUtils.MEDIA_ID_MENU_AUDIO, null);
    }

    @Override
    public void onLoadChildren(@NonNull String parentId,
            @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
        Log.d(TAG, "MEDIA ID: " + parentId);

        List<String> mediaId = MediaUtils.parseMediaId(parentId);

        if (mediaId.isEmpty()) {
            Log.e(TAG, "Unable to parse media ID: " + parentId);
            return;
        }

        String root = mediaId.get(0);

        switch (root) {
        case MediaUtils.MEDIA_ID_COLLECTIONS:
            result.detach();
            getCollections(result);
            break;

        case MediaUtils.MEDIA_ID_COLLECTION:
            if (mediaId.size() > 1) {
                result.detach();
                getMediaElementForCollection(mediaId.get(1), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ALBUM:
            if (mediaId.size() > 1) {
                result.detach();
                getMediaElementForAlbum(mediaId.get(1), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ARTIST_ALBUM:
            if (mediaId.size() > 2) {
                result.detach();
                getMediaElementForAlbumAndArtist(mediaId.get(1), mediaId.get(2), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ALBUM_ARTIST_ALBUM:
            if (mediaId.size() > 2) {
                result.detach();
                getMediaElementForAlbumAndAlbumArtist(mediaId.get(1), mediaId.get(2), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ALBUM_ARTIST:
            if (mediaId.size() > 1) {
                result.detach();
                getAlbumsByAlbumArtist(mediaId.get(1), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ALBUM_ARTISTS:
            result.detach();
            getAlbumArtists(result);
            break;

        case MediaUtils.MEDIA_ID_ALBUMS:
            result.detach();
            getAlbums(result);
            break;

        case MediaUtils.MEDIA_ID_ARTIST:
            if (mediaId.size() > 1) {
                result.detach();
                getAlbumsByArtist(mediaId.get(1), result);
            }

            break;

        case MediaUtils.MEDIA_ID_ARTISTS:
            result.detach();
            getArtists(result);
            break;

        case MediaUtils.MEDIA_ID_AUDIO:
            break;

        case MediaUtils.MEDIA_ID_DIRECTORY:
        case MediaUtils.MEDIA_ID_DIRECTORY_AUDIO:
        case MediaUtils.MEDIA_ID_DIRECTORY_VIDEO:
            if (mediaId.size() > 1) {
                result.detach();
                getMediaElementContents(UUID.fromString(mediaId.get(1)), result);
            }

            break;

        case MediaUtils.MEDIA_ID_FOLDER:
        case MediaUtils.MEDIA_ID_FOLDER_AUDIO:
        case MediaUtils.MEDIA_ID_FOLDER_VIDEO:
            if (mediaId.size() > 1) {
                result.detach();
                getMediaFolderContents(UUID.fromString(mediaId.get(1)), result);
            }

            break;

        case MediaUtils.MEDIA_ID_FOLDERS:
            result.detach();
            getMediaFolders(result);
            break;

        case MediaUtils.MEDIA_ID_FOLDERS_AUDIO:
            break;

        case MediaUtils.MEDIA_ID_FOLDERS_VIDEO:
            break;

        case MediaUtils.MEDIA_ID_MENU_AUDIO:
            result.sendResult(getAudioMenu());
            break;

        case MediaUtils.MEDIA_ID_MENU_VIDEO:
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_ADDED:
            result.detach();
            getRecentlyAdded(null, result);
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_PLAYED:
            result.detach();
            getRecentlyPlayed(null, result);
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_ADDED_AUDIO:
            result.detach();
            getRecentlyAdded(MediaElement.DirectoryMediaType.AUDIO, result);
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_ADDED_VIDEO:
            result.detach();
            getRecentlyAdded(MediaElement.DirectoryMediaType.VIDEO, result);
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_PLAYED_AUDIO:
            result.detach();
            getRecentlyPlayed(MediaElement.DirectoryMediaType.AUDIO, result);
            break;

        case MediaUtils.MEDIA_ID_RECENTLY_PLAYED_VIDEO:
            result.detach();
            getRecentlyPlayed(MediaElement.DirectoryMediaType.VIDEO, result);
            break;

        case MediaUtils.MEDIA_ID_VIDEO:
            break;

        case MediaUtils.MEDIA_ID_PLAYLISTS:
            result.detach();
            getPlaylists(result);
            break;

        case MediaUtils.MEDIA_ID_PLAYLIST:
            if (mediaId.size() > 1) {
                result.detach();
                getPlaylistContents(UUID.fromString(mediaId.get(1)), result);
            }

            break;

        default:
            Log.w(TAG, "Media ID not recognised: " + parentId);
            break;
        }
    }

    /**
     * Fetch a list of all Media Folders.
     */
    private void getMediaFolders(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getMediaFolders(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> folders = new ArrayList<>();
                MediaFolder folder = parser.fromJson(response.toString(), MediaFolder.class);

                if (folder.getType() == MediaFolder.ContentType.AUDIO
                        || folder.getType() == MediaFolder.ContentType.VIDEO) {
                    folders.add(createMediaItemFromMediaFolder(folder));
                }

                result.sendResult(folders);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> folders = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaFolder folder = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaFolder.class);

                        if (folder.getType() == MediaFolder.ContentType.AUDIO
                                || folder.getType() == MediaFolder.ContentType.VIDEO) {
                            folders.add(createMediaItemFromMediaFolder(folder));
                        }
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(folders);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve media folder list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve media folder list");
            }
        });
    }

    /**
     * Fetch the contents of a given Media Folder.
     */
    private void getMediaFolderContents(UUID id, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaFolderContents(getApplicationContext(), id, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve media folder contents");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve media folder contents");
            }
        });
    }

    /**
     * Fetch a list of playlists.
     */
    private void getPlaylists(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getPlaylists(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> playlists = new ArrayList<>();

                Playlist playlist = parser.fromJson(response.toString(), Playlist.class);
                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_PLAYLIST + MediaUtils.SEPARATOR + playlist.getID())
                        .setTitle(playlist.getName())
                        .setSubtitle(playlist.getDescription() == null ? "" : playlist.getDescription())
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_playlist))
                        .build();

                playlists.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE));

                Log.d(TAG, "Playlist: " + playlist.getName());
                result.sendResult(playlists);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> playlists = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        Playlist playlist = parser.fromJson(response.getJSONObject(i).toString(), Playlist.class);
                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_PLAYLIST + MediaUtils.SEPARATOR + playlist.getID())
                                .setTitle(playlist.getName())
                                .setSubtitle(playlist.getDescription() == null ? "" : playlist.getDescription())
                                .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_playlist))
                                .build();

                        playlists.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_PLAYABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(playlists);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve playlists");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve playlists");
            }
        });
    }

    /**
     * Fetch the contents of a given Playlist.
     */
    private void getPlaylistContents(UUID id, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getPlaylistContents(getApplicationContext(), id, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve playlist contents");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve playlist contents");
            }
        });
    }

    /**
     * Fetch the contents of a given Media Folder.
     */
    private void getMediaElementContents(UUID id,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaElementContents(getApplicationContext(), id, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve media directory contents");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve media directory contents");
            }
        });
    }

    /**
     * Fetch recently played media elements
     */
    private void getRecentlyPlayed(Byte type, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getRecentlyPlayed(getApplicationContext(), FETCH_LIMIT, type, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve recently played list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve recently played list");
            }
        });
    }

    /**
     * Fetch recently added media elements
     */
    private void getRecentlyAdded(Byte type, @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getRecentlyAdded(getApplicationContext(), FETCH_LIMIT, type, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve recently added list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve recently added list");
            }
        });
    }

    /**
     * Fetch a list of collections.
     */
    private void getCollections(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getCollections(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> collections = new ArrayList<>();
                String collection = response.toString();
                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_COLLECTION + MediaUtils.SEPARATOR + collection)
                        .setTitle(collection)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_movie))
                        .build();

                collections.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(collections);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> collections = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String collection = response.getString(i);
                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_COLLECTION + MediaUtils.SEPARATOR + collection)
                                .setTitle(collection).setIconUri(ResourceUtils
                                        .getUriToResource(getApplicationContext(), R.drawable.ic_movie))
                                .build();

                        collections.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(collections);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve collection list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve collection list");
            }
        });
    }

    /**
     * Fetch a list of artists.
     */
    private void getArtists(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getArtists(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> artists = new ArrayList<>();
                String artist = response.toString();
                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_ARTIST + MediaUtils.SEPARATOR + artist).setTitle(artist)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_artist))
                        .build();

                artists.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(artists);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> artists = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String artist = response.getString(i);
                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_ARTIST + MediaUtils.SEPARATOR + artist)
                                .setTitle(artist).setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_artist))
                                .build();

                        artists.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(artists);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve artist list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve artist list");
            }
        });
    }

    /**
     * Fetch a list of album artists.
     */
    private void getAlbumArtists(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getAlbumArtists(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> artists = new ArrayList<>();
                String artist = response.toString();
                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_ALBUM_ARTIST + MediaUtils.SEPARATOR + artist)
                        .setTitle(artist)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_artist))
                        .build();

                artists.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(artists);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> artists = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String artist = response.getString(i);
                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_ALBUM_ARTIST + MediaUtils.SEPARATOR + artist)
                                .setTitle(artist).setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_artist))
                                .build();

                        artists.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(artists);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve album artist list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve album artist list");
            }
        });
    }

    /**
     * Fetch a list of albums.
     */
    private void getAlbums(@NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getAlbums(getApplicationContext(), new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();
                String album = response.toString();
                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_ALBUM + MediaUtils.SEPARATOR + album).setTitle(album)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_album))
                        .build();

                albums.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(albums);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String album = response.getString(i);
                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_ALBUM + MediaUtils.SEPARATOR + album)
                                .setTitle(album).setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_album))
                                .build();

                        albums.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(albums);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve album list");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve album list");
            }
        });
    }

    /**
     * Fetch a list of albums for artist.
     */
    private void getAlbumsByArtist(final String artist,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getAlbumsByArtist(getApplicationContext(), artist, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();
                String album = response.toString();
                Bundle extras = new Bundle();

                // Save artist so we can retrieve media elements
                extras.putString(MediaUtils.MEDIA_ID_ARTIST, artist);

                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_ARTIST_ALBUM + MediaUtils.SEPARATOR + artist
                                + MediaUtils.SEPARATOR + album)
                        .setTitle(album)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_album))
                        .setExtras(extras).build();

                albums.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(albums);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String album = response.getString(i);
                        Bundle extras = new Bundle();

                        // Save artist so we can retrieve media elements
                        extras.putString(MediaUtils.MEDIA_ID_ARTIST, artist);

                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_ARTIST_ALBUM + MediaUtils.SEPARATOR + artist
                                        + MediaUtils.SEPARATOR + album)
                                .setTitle(album).setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_album))
                                .build();

                        albums.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(albums);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve album list for artist: " + artist);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve album list for artist: " + artist);
            }
        });
    }

    /**
     * Fetch a list of albums for album artist.
     */
    private void getAlbumsByAlbumArtist(final String artist,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {

        restService.getAlbumsByAlbumArtist(getApplicationContext(), artist, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();
                String album = response.toString();
                Bundle extras = new Bundle();

                // Save artist so we can retrieve media elements
                extras.putString(MediaUtils.MEDIA_ID_ARTIST, artist);

                MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_ALBUM_ARTIST_ALBUM + MediaUtils.SEPARATOR + artist
                                + MediaUtils.SEPARATOR + album)
                        .setTitle(album)
                        .setIconUri(ResourceUtils.getUriToResource(getApplicationContext(), R.drawable.ic_album))
                        .setExtras(extras).build();

                albums.add(
                        new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));

                result.sendResult(albums);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                List<MediaBrowserCompat.MediaItem> albums = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        String album = response.getString(i);
                        Bundle extras = new Bundle();

                        // Save artist so we can retrieve media elements
                        extras.putString(MediaUtils.MEDIA_ID_ARTIST, artist);

                        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                                .setMediaId(MediaUtils.MEDIA_ID_ALBUM_ARTIST_ALBUM + MediaUtils.SEPARATOR + artist
                                        + MediaUtils.SEPARATOR + album)
                                .setTitle(album).setIconUri(ResourceUtils.getUriToResource(getApplicationContext(),
                                        R.drawable.ic_album))
                                .build();

                        albums.add(new MediaBrowserCompat.MediaItem(description,
                                MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(albums);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve album list for album artist: " + artist);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve album list for album artist: " + artist);
            }
        });
    }

    /**
     * Fetch media elements for album and artist
     */
    private void getMediaElementForAlbumAndArtist(final String artist, final String album,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaElementsByArtistAndAlbum(getApplicationContext(), artist, album,
                new JsonHttpResponseHandler() {

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONObject response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                        MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                        result.sendResult(elements);
                    }

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONArray response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                        for (int i = 0; i < response.length(); i++) {
                            try {
                                MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                        MediaElement.class);
                                elements.add(createMediaItemFromMediaElement(element));
                            } catch (JSONException e) {
                                Log.e(TAG, "Failed to process JSON", e);
                            }
                        }

                        result.sendResult(elements);
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONObject response) {
                        Log.w(TAG, "Failed to retrieve media elements for album '" + album + "' and artist '"
                                + artist + "'");
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONArray response) {
                        Log.w(TAG, "Failed to retrieve media elements for album '" + album + "' and artist '"
                                + artist + "'");
                    }
                });
    }

    /**
     * Fetch media elements for album and album artist
     */
    private void getMediaElementForAlbumAndAlbumArtist(final String artist, final String album,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaElementsByAlbumArtistAndAlbum(getApplicationContext(), artist, album,
                new JsonHttpResponseHandler() {

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONObject response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                        MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                        result.sendResult(elements);
                    }

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONArray response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                        for (int i = 0; i < response.length(); i++) {
                            try {
                                MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                        MediaElement.class);
                                elements.add(createMediaItemFromMediaElement(element));
                            } catch (JSONException e) {
                                Log.e(TAG, "Failed to process JSON", e);
                            }
                        }

                        result.sendResult(elements);
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONObject response) {
                        Log.w(TAG, "Failed to retrieve media elements for album '" + album + "' and album artist '"
                                + artist + "'");
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONArray response) {
                        Log.w(TAG, "Failed to retrieve media elements for album '" + album + "' and album artist '"
                                + artist + "'");
                    }
                });
    }

    /**
     * Fetch media elements for an album
     */
    private void getMediaElementForAlbum(final String album,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaElementsByAlbum(getApplicationContext(), album, new JsonHttpResponseHandler() {

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONObject response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                elements.add(createMediaItemFromMediaElement(element));
                result.sendResult(elements);
            }

            @Override
            public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    JSONArray response) {
                Gson parser = new Gson();
                List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                for (int i = 0; i < response.length(); i++) {
                    try {
                        MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to process JSON", e);
                    }
                }

                result.sendResult(elements);
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONObject response) {
                Log.w(TAG, "Failed to retrieve media elements for album '" + album + "'");
            }

            @Override
            public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                    Throwable throwable, JSONArray response) {
                Log.w(TAG, "Failed to retrieve media elements for album '" + album + "'");
            }
        });
    }

    /**
     * Fetch media elements for a collection
     */
    private void getMediaElementForCollection(final String collection,
            @NonNull final Result<List<MediaBrowserCompat.MediaItem>> result) {
        restService.getMediaElementsByCollection(getApplicationContext(), collection,
                new JsonHttpResponseHandler() {

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONObject response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();
                        MediaElement element = parser.fromJson(response.toString(), MediaElement.class);
                        elements.add(createMediaItemFromMediaElement(element));
                        result.sendResult(elements);
                    }

                    @Override
                    public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            JSONArray response) {
                        Gson parser = new Gson();
                        List<MediaBrowserCompat.MediaItem> elements = new ArrayList<>();

                        for (int i = 0; i < response.length(); i++) {
                            try {
                                MediaElement element = parser.fromJson(response.getJSONObject(i).toString(),
                                        MediaElement.class);
                                elements.add(createMediaItemFromMediaElement(element));
                            } catch (JSONException e) {
                                Log.e(TAG, "Failed to process JSON", e);
                            }
                        }

                        result.sendResult(elements);
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONObject response) {
                        Log.w(TAG, "Failed to retrieve media elements for collection '" + collection + "'");
                    }

                    @Override
                    public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers,
                            Throwable throwable, JSONArray response) {
                        Log.w(TAG, "Failed to retrieve media elements for collection '" + collection + "'");
                    }
                });
    }

    /**
     * Convert Media Folder to a MediaItem for media browser compatibility.
     */
    private MediaBrowserCompat.MediaItem createMediaItemFromMediaFolder(@NonNull MediaFolder folder) {
        MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
                .setMediaId(MediaUtils.MEDIA_ID_FOLDER + MediaUtils.SEPARATOR + String.valueOf(folder.getID()))
                .setTitle(folder.getName())
                .setSubtitle(String.format(getString(R.string.media_folder_subtitle), folder.getFiles(),
                        folder.getFolders()))
                .setIconUri(Uri.parse(RESTService.getInstance().getAddress() + "/image/" + folder.getID() + "/cover"
                        + "?folder=true"))
                .build();

        return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
    }

    /**
     * Convert Media Element to a Media Item for Media Browser compatibility
     */
    private MediaBrowserCompat.MediaItem createMediaItemFromMediaElement(@NonNull MediaElement element) {
        switch (element.getType()) {
        case MediaElement.MediaElementType.DIRECTORY:
            MediaDescriptionCompat dirDescription = null;

            if (element.getDirectoryType() == MediaElement.DirectoryMediaType.NONE
                    || element.getDirectoryType() == MediaElement.DirectoryMediaType.MIXED) {
                dirDescription = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_DIRECTORY
                                + MediaUtils.SEPARATOR + String.valueOf(element.getID()))
                        .setTitle(element.getTitle())
                        .setIconUri(Uri.parse(
                                RESTService.getInstance().getAddress() + "/image/" + element.getID() + "/cover"))
                        .build();
            } else if (element.getDirectoryType() == MediaElement.DirectoryMediaType.VIDEO) {
                dirDescription = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_DIRECTORY_VIDEO + MediaUtils.SEPARATOR
                                + String.valueOf(element.getID()))
                        .setTitle(element.getTitle())
                        .setSubtitle(element.getCollection() == null ? "" : element.getCollection())
                        .setDescription(element.getDescription() == null ? "" : element.getDescription())
                        .setIconUri(Uri.parse(
                                RESTService.getInstance().getAddress() + "/image/" + element.getID() + "/cover"))
                        .build();
            } else if (element.getDirectoryType() == MediaElement.DirectoryMediaType.AUDIO) {
                dirDescription = new MediaDescriptionCompat.Builder()
                        .setMediaId(MediaUtils.MEDIA_ID_DIRECTORY_AUDIO + MediaUtils.SEPARATOR
                                + String.valueOf(element.getID()))
                        .setTitle(element.getTitle())
                        .setSubtitle(element.getArtist() == null ? "" : element.getArtist())
                        .setDescription(element.getYear() == null ? "" : element.getYear().toString())
                        .setIconUri(Uri.parse(
                                RESTService.getInstance().getAddress() + "/image/" + element.getID() + "/cover"))
                        .build();
            }

            return new MediaBrowserCompat.MediaItem(dirDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);

        case MediaElement.MediaElementType.AUDIO:
            return new MediaBrowserCompat.MediaItem(MediaUtils.getMediaDescription(element),
                    MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);

        case MediaElement.MediaElementType.VIDEO:
            return new MediaBrowserCompat.MediaItem(MediaUtils.getMediaDescription(element),
                    MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);

        default:
            Log.i(TAG, "Unknown media type for media element with id " + element.getID());
            return null;
        }
    }

    /**
     * Build Audio menu media items
     */
    private List<MediaBrowserCompat.MediaItem> getAudioMenu() {
        List<MediaDescriptionCompat> descriptions = new ArrayList<>();
        List<MediaBrowserCompat.MediaItem> items = new ArrayList<>();

        if (!isOnline || !isConnected) {
            return null;
        }

        // Recently Added
        descriptions.add(new MediaDescriptionCompat.Builder().setMediaId(MediaUtils.MEDIA_ID_RECENTLY_ADDED_AUDIO)
                .setTitle(getString(R.string.heading_recently_added)).build());

        // Recently Played
        descriptions.add(new MediaDescriptionCompat.Builder().setMediaId(MediaUtils.MEDIA_ID_RECENTLY_PLAYED_AUDIO)
                .setTitle(getString(R.string.heading_recently_played)).build());

        // Playlists
        descriptions.add(new MediaDescriptionCompat.Builder().setMediaId(MediaUtils.MEDIA_ID_PLAYLISTS)
                .setTitle(getString(R.string.heading_playlists)).build());

        // Add menu items
        for (MediaDescriptionCompat description : descriptions) {
            items.add(new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
        }

        return items;
    }
}