com.fastbootmobile.encore.app.fragments.ArtistFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.fastbootmobile.encore.app.fragments.ArtistFragment.java

Source

/*
 * Copyright (C) 2014 Fastboot Mobile, LLC.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see <http://www.gnu.org/licenses>.
 */

package com.fastbootmobile.encore.app.fragments;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.PagerTabStrip;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.CardView;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.echonest.api.v4.Biography;
import com.echonest.api.v4.EchoNestException;
import com.fastbootmobile.encore.api.echonest.EchoNest;
import com.fastbootmobile.encore.app.AppActivity;
import com.fastbootmobile.encore.app.ArtistActivity;
import com.fastbootmobile.encore.app.R;
import com.fastbootmobile.encore.app.adapters.ArtistsAdapter;
import com.fastbootmobile.encore.app.ui.AlbumArtImageView;
import com.fastbootmobile.encore.app.ui.MaterialTransitionDrawable;
import com.fastbootmobile.encore.app.ui.ObservableScrollView;
import com.fastbootmobile.encore.app.ui.ParallaxScrollView;
import com.fastbootmobile.encore.app.ui.PlayPauseDrawable;
import com.fastbootmobile.encore.app.ui.WrapContentHeightViewPager;
import com.fastbootmobile.encore.art.AlbumArtHelper;
import com.fastbootmobile.encore.art.RecyclingBitmapDrawable;
import com.fastbootmobile.encore.framework.PlaybackProxy;
import com.fastbootmobile.encore.framework.PluginsLookup;
import com.fastbootmobile.encore.framework.Suggestor;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.SearchResult;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.ILocalCallback;
import com.fastbootmobile.encore.providers.IMusicProvider;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import com.fastbootmobile.encore.providers.ProviderConnection;
import com.fastbootmobile.encore.providers.ProviderIdentifier;
import com.fastbootmobile.encore.service.BasePlaybackCallback;
import com.fastbootmobile.encore.service.PlaybackService;
import com.fastbootmobile.encore.utils.Utils;
import com.getbase.floatingactionbutton.FloatingActionButton;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
 * Fragment showing artist information: Tracks, similar artists, and biography
 */
public class ArtistFragment extends Fragment implements ILocalCallback {
    private static final String TAG = "ArtistFragment";

    private static final int FRAGMENT_ID_TRACKS = 0;
    private static final int FRAGMENT_ID_SIMILAR = 1;
    private static final int FRAGMENT_ID_BIOGRAPHY = 2;
    private static final int FRAGMENT_COUNT = 3;

    private static final int ANIMATION_DURATION = 300;
    private static final DecelerateInterpolator mInterpolator = new DecelerateInterpolator();

    private Bitmap mHeroImage;
    private int mBackgroundColor;
    private Artist mArtist;
    private ParallaxScrollView mRootView;
    private Handler mHandler;
    private PlayPauseDrawable mFabDrawable;
    private boolean mFabShouldResume;
    private ArtistTracksFragment mArtistTracksFragment;
    private ArtistInfoFragment mArtistInfoFragment;
    private ArtistSimilarFragment mArtistSimilarFragment;
    private FloatingActionButton mFabPlay;
    private RecyclingBitmapDrawable mLogoBitmap;
    private ImageView mHeroImageView;

    private Runnable mUpdateAlbumsRunnable = new Runnable() {
        @Override
        public void run() {
            ProviderAggregator aggregator = ProviderAggregator.getDefault();
            mArtist = aggregator.retrieveArtist(mArtist.getRef(), mArtist.getProvider());

            mArtistTracksFragment.loadRecommendation();
            mArtistTracksFragment.loadAlbums(false);
        }
    };

    private BasePlaybackCallback mPlaybackCallback = new BasePlaybackCallback() {
        @Override
        public void onSongStarted(final boolean buffering, Song s) throws RemoteException {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE);
                    mFabDrawable.setBuffering(buffering);
                }
            });
        }

        @Override
        public void onPlaybackPause() throws RemoteException {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
                    mFabDrawable.setBuffering(false);
                }
            });
        }

        @Override
        public void onPlaybackResume() throws RemoteException {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE);
                    mFabDrawable.setBuffering(false);
                }
            });
        }
    };

    /**
     * Class handling the ViewPager for the tabs
     */
    private class ViewPagerAdapter extends FragmentPagerAdapter {
        private boolean mHasRosetta;

        public ViewPagerAdapter(FragmentManager fm) {
            super(fm);
            // Depending on whether we have a rosetta-enabled provider or not, we will
            // show or not the Similar tab.
            mHasRosetta = ProviderAggregator.getDefault().getRosettaStonePrefix().size() > 0;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Fragment getItem(int i) {
            // We're deliberately not using constants in this switch because values may be two
            // different things.
            switch (i) {
            case 0: // Tracks tab
                return mArtistTracksFragment;

            case 1: // Either similar, or biography tab
                if (mHasRosetta) {
                    return mArtistSimilarFragment;
                } else {
                    return mArtistInfoFragment;
                }

            case 2: // Biography tab in case similar is enabled
                return mArtistInfoFragment;
            }

            // should never happen
            throw new IllegalStateException("We should never be here, i=" + i);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public CharSequence getPageTitle(int position) {
            if (position == 0) {
                return getString(R.string.tracks).toUpperCase();
            } else if (position == 1) {
                if (mHasRosetta) {
                    return getString(R.string.Similar).toUpperCase();
                } else {
                    return getString(R.string.biography).toUpperCase();
                }
            } else if (position == 2) {
                return getString(R.string.biography).toUpperCase();
            }

            return "Error";
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int getCount() {
            // Similar only works if we have one provider supporting Rosetta Stone
            if (mHasRosetta) {
                return FRAGMENT_COUNT;
            } else {
                return FRAGMENT_COUNT - 1;
            }
        }
    }

    /**
     * Album art helper for each entry in the Tracks view
     */
    private static class AlbumArtLoadListener implements AlbumArtImageView.OnArtLoadedListener {
        private View mRootView;
        private Handler mHandler;

        public AlbumArtLoadListener(View rootView) {
            mRootView = rootView;
            mHandler = new Handler();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onArtLoaded(AlbumArtImageView view, BitmapDrawable drawable) {
            if (drawable == null || drawable.getBitmap() == null) {
                return;
            }

            Palette.from(drawable.getBitmap()).generate(new Palette.PaletteAsyncListener() {
                @Override
                public void onGenerated(Palette palette) {
                    Palette.Swatch vibrant = palette.getVibrantSwatch();

                    if (vibrant != null && mRootView != null) {
                        mRootView.setBackgroundColor(vibrant.getRgb());

                        float luminance = vibrant.getHsl()[2];

                        final TextView tvArtist = (TextView) mRootView.findViewById(R.id.tvArtistSuggestionArtist);
                        final TextView tvTitle = (TextView) mRootView.findViewById(R.id.tvArtistSuggestionTitle);
                        final Button btnPlay = (Button) mRootView.findViewById(R.id.btnArtistSuggestionPlay);

                        final int color = luminance < 0.6f ? 0xFFFFFFFF : 0xFF333333;

                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                tvArtist.setTextColor(color);
                                tvTitle.setTextColor(color);
                                btnPlay.setTextColor(color);
                            }
                        });
                    }
                }
            });

        }
    }

    /**
     * Click listener on Album group entries
     */
    private static class AlbumGroupClickListener implements View.OnClickListener {
        private Album mAlbum;
        private LinearLayout mContainer;
        private LinearLayout mItemHost;
        private View mHeader;
        private boolean mOpen;
        private View mHeaderDivider;
        private View mLastItemDivider;
        private ParallaxScrollView mRootView;
        private Context mContext;
        private ArtistTracksFragment mTracksFragment;
        private Handler mHandler;

        public AlbumGroupClickListener(Album a, ParallaxScrollView rootView, LinearLayout container, View header,
                ArtistTracksFragment tracksFragment) {
            mAlbum = a;
            mRootView = rootView;
            mContainer = container;
            mOpen = false;
            mHeader = header;
            mContext = header.getContext();
            mTracksFragment = tracksFragment;
            mHandler = new Handler();

            mHeaderDivider = header.findViewById(R.id.divider);
            mHeaderDivider.setVisibility(View.VISIBLE);
            mHeaderDivider.setAlpha(0.0f);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onClick(View view) {
            toggle();
        }

        /**
         * Toggles an album group visibility
         */
        public void toggle() {
            if (mOpen) {
                mItemHost.startAnimation(Utils.animateExpand(mItemHost, false));
                mOpen = false;

                mLastItemDivider.animate().alpha(0.0f).setDuration(ANIMATION_DURATION)
                        .setInterpolator(mInterpolator).start();
                mHeaderDivider.animate().alpha(0.0f).setDuration(ANIMATION_DURATION).setInterpolator(mInterpolator)
                        .start();
            } else {
                if (mItemHost == null) {
                    mItemHost = new LinearLayout(mContext);
                    mItemHost.setOrientation(LinearLayout.VERTICAL);

                    // We insert the view below the group
                    int index = ((LinearLayout) mHeader.getParent()).indexOfChild(mHeader);
                    mContainer.addView(mItemHost, index + 1);

                    mTracksFragment.showAlbumTracks(mAlbum, mItemHost);

                    // Add the divider at the end
                    LayoutInflater inflater = (LayoutInflater) mContext
                            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                    mLastItemDivider = inflater.inflate(R.layout.divider, mItemHost, false);
                    mItemHost.addView(mLastItemDivider);
                }
                mItemHost.startAnimation(Utils.animateExpand(mItemHost, true));
                mHeaderDivider.animate().alpha(1.0f).setDuration(ANIMATION_DURATION).setInterpolator(mInterpolator)
                        .start();

                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mRootView.smoothScrollBy(0, Utils.dpToPx(mContext.getResources(), 240));
                    }
                }, 300);

                mOpen = true;
            }
        }
    }

    /**
     * Default constructor
     */
    public ArtistFragment() {
        mFabShouldResume = false;
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public void notifyClosing() {
        Utils.animateScale(mFabPlay, true, false);
        final TextView tvArtist = (TextView) mRootView.findViewById(R.id.tvArtist);
        final PagerTabStrip strip = (PagerTabStrip) mRootView.findViewById(R.id.pagerArtistStrip);

        if (!Utils.hasLollipop()) {
            tvArtist.animate().alpha(0.0f).setStartDelay(0).setDuration(ArtistActivity.BACK_DELAY).start();
        }
        strip.animate().alpha(0.0f).setStartDelay(0).translationY(-20).setDuration(ArtistActivity.BACK_DELAY)
                .start();
    }

    public void scrollToTop() {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mRootView.smoothScrollTo(0, 0);
            }
        });
    }

    /**
     * Returns a view in the fragment
     *
     * @param id The layout item ID
     * @return The view if found, null otherwise
     */
    public View findViewById(int id) {
        return mRootView.findViewById(id);
    }

    /**
     * Sets the main arguments for the fragment
     *
     * @param hero   The hero header image bitmap
     * @param extras The intent bundle extras
     */
    public void setArguments(Bitmap hero, Bundle extras) {
        mHeroImage = hero;
        mBackgroundColor = extras.getInt(ArtistActivity.EXTRA_BACKGROUND_COLOR, 0xFF333333);
        final String artistRef = extras.getString(ArtistActivity.EXTRA_ARTIST);
        final ProviderIdentifier provider = extras.getParcelable(ArtistActivity.EXTRA_PROVIDER);
        mArtist = ProviderAggregator.getDefault().retrieveArtist(artistRef, provider);

        if (mArtist == null) {
            Log.e(TAG, "No cache entry or provider hit for " + artistRef + "!");
            throw new IllegalStateException("Artist is null in ArtistFragment arguments!");
        }

        // Prepare the palette to colorize the FAB
        if (mHeroImage != null) {
            generateHeroPalette();
        }
    }

    private void generateHeroPalette() {
        if (mHeroImage != null && !mHeroImage.isRecycled()) {
            Palette.from(mHeroImage).generate(new Palette.PaletteAsyncListener() {
                @Override
                public void onGenerated(final Palette palette) {
                    final Palette.Swatch normalColor = palette.getDarkMutedSwatch();
                    if (normalColor != null && mRootView != null) {
                        mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (mRootView != null) {
                                    final Palette.Swatch pressedColor = palette.getDarkVibrantSwatch();
                                    mFabPlay.setNormalColor(normalColor.getRgb());
                                    if (pressedColor != null) {
                                        mFabPlay.setPressedColor(pressedColor.getRgb());
                                    } else {
                                        mFabPlay.setPressedColor(normalColor.getRgb());
                                    }
                                }
                            }
                        });
                    }
                }
            });
        }
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mHandler = new Handler();

        // Setup the inside fragments
        mArtistTracksFragment = new ArtistTracksFragment();
        mArtistTracksFragment.setParentFragment(this);

        mArtistInfoFragment = new ArtistInfoFragment();
        mArtistInfoFragment.setArguments(mArtist);

        mArtistSimilarFragment = new ArtistSimilarFragment();
        mArtistSimilarFragment.setArguments(mArtist);

        // Inflate the main fragment view
        mRootView = (ParallaxScrollView) inflater.inflate(R.layout.fragment_artist, container, false);

        // Set the hero image and artist from arguments
        mHeroImageView = (ImageView) mRootView.findViewById(R.id.ivHero);
        if (mHeroImage != null) {
            mHeroImageView.setImageBitmap(mHeroImage);

            // The hero image that comes from a transition might be low in quality, so load
            // the higher quality and fade it in
            loadArt(false);
        } else {
            // Display placeholder and try to get the real art
            mHeroImageView.setImageResource(R.drawable.album_placeholder);
            loadArt(true);
        }

        final TextView tvArtist = (TextView) mRootView.findViewById(R.id.tvArtist);
        tvArtist.setBackgroundColor(mBackgroundColor);
        tvArtist.setText(mArtist.getName());

        final PagerTabStrip strip = (PagerTabStrip) mRootView.findViewById(R.id.pagerArtistStrip);
        strip.setDrawFullUnderline(false);
        strip.setAlpha(0.0f);
        strip.setTranslationY(-20);
        strip.animate().alpha(1.0f).setDuration(ANIMATION_DURATION).setStartDelay(500).translationY(0).start();

        if (!Utils.hasLollipop()) {
            tvArtist.setAlpha(0);
            tvArtist.animate().alpha(1).setDuration(ANIMATION_DURATION).setStartDelay(500).start();
        }

        // Setup the subfragments pager
        final WrapContentHeightViewPager pager = (WrapContentHeightViewPager) mRootView
                .findViewById(R.id.pagerArtist);
        pager.setAdapter(new ViewPagerAdapter(getChildFragmentManager()));
        pager.setOffscreenPageLimit(FRAGMENT_COUNT);

        pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int i) {
                if (mRootView.getScrollY() > tvArtist.getTop()) {
                    mRootView.smoothScrollTo(0, tvArtist.getTop());
                }
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        pager.setMinimumHeight(500);
                        pager.requestLayout();
                    }
                });

                boolean hasRosetta = ProviderAggregator.getDefault().getRosettaStonePrefix().size() > 0;

                if (hasRosetta) {
                    if (i == FRAGMENT_ID_BIOGRAPHY) {
                        mArtistInfoFragment.notifyActive();
                    } else if (i == FRAGMENT_ID_SIMILAR) {
                        mArtistSimilarFragment.notifyActive();
                    }
                } else {
                    if (i == FRAGMENT_ID_SIMILAR) {
                        // This is actually BIOGRAPHY if rosetta is not available
                        mArtistInfoFragment.notifyActive();
                    }
                }
            }

            @Override
            public void onPageScrollStateChanged(int i) {
            }
        });

        mRootView.setOnScrollListener(new ObservableScrollView.ScrollViewListener() {
            @Override
            public void onScroll(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
                final ActionBar ab = ((AppActivity) getActivity()).getSupportActionBar();
                if (ab != null) {
                    if (y >= tvArtist.getTop()) {
                        ab.hide();
                    } else {
                        ab.show();
                    }
                }
            }
        });

        // Setup the source logo
        final ImageView ivSource = (ImageView) mRootView.findViewById(R.id.ivSourceLogo);
        mLogoBitmap = PluginsLookup.getDefault().getCachedLogo(getResources(), mArtist);
        ivSource.setImageDrawable(mLogoBitmap);

        // Outline is required for the FAB shadow to be actually oval
        mFabPlay = (FloatingActionButton) mRootView.findViewById(R.id.fabPlay);
        showFab(false, false);

        // Set the FAB animated drawable
        mFabDrawable = new PlayPauseDrawable(getResources(), 1);
        mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
        mFabDrawable.setYOffset(6);

        final Song currentTrack = PlaybackProxy.getCurrentTrack();
        if (currentTrack != null && currentTrack.getArtist() != null
                && currentTrack.getArtist().equals(mArtist.getRef())) {
            int state = PlaybackProxy.getState();
            if (state == PlaybackService.STATE_PLAYING) {
                mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE);
            } else if (state == PlaybackService.STATE_PAUSED) {
                mFabShouldResume = true;
            } else if (state == PlaybackService.STATE_BUFFERING) {
                mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
                mFabDrawable.setBuffering(true);
                mFabShouldResume = true;
            } else if (state == PlaybackService.STATE_PAUSING) {
                mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE);
                mFabDrawable.setBuffering(true);
                mFabShouldResume = true;
            }
        }

        mFabPlay.setImageDrawable(mFabDrawable);
        mFabPlay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (mFabDrawable.getCurrentShape() == PlayPauseDrawable.SHAPE_PLAY) {
                    if (mFabShouldResume) {
                        PlaybackProxy.play();
                    } else {
                        mArtistTracksFragment.playRecommendation();
                    }
                } else {
                    mFabDrawable.setShape(PlayPauseDrawable.SHAPE_PAUSE);
                    mFabShouldResume = true;
                    PlaybackProxy.pause();
                }
            }
        });

        return mRootView;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onResume() {
        super.onResume();

        // Register for updates
        PlaybackProxy.addCallback(mPlaybackCallback);
        ProviderAggregator.getDefault().addUpdateCallback(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onPause() {
        super.onPause();

        mHandler.removeCallbacks(mUpdateAlbumsRunnable);

        // Unregister callbacks
        PlaybackProxy.removeCallback(mPlaybackCallback);
        ProviderAggregator.getDefault().removeUpdateCallback(this);
    }

    /**
     * Shows or hides the play Floating Action Button
     *
     * @param animate Whether or not to animate the transition
     * @param visible Whether or not to display the button
     */
    private void showFab(final boolean animate, final boolean visible) {
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Utils.animateScale(mFabPlay, animate, visible);
            }
        });
    }

    /**
     * Sets the play FAB drawable shape
     *
     * @param shape The shape to display (one of {@link com.fastbootmobile.encore.app.ui.PlayPauseDrawable}
     *              constants
     */
    private void setFabShape(int shape) {
        mFabDrawable.setShape(shape);
    }

    /**
     * Sets whether or not tapping the FAB in "Play" shape will start playing from scratch or resume
     * the current playback.
     *
     * @param shouldResume True to resume, false to play from scratch
     */
    private void setFabShouldResume(boolean shouldResume) {
        mFabShouldResume = shouldResume;
    }

    /**
     * @return The artist displayed by this fragment
     */
    public Artist getArtist() {
        return mArtist;
    }

    private void loadArt(final boolean materialTransition) {
        AlbumArtHelper.retrieveAlbumArt(getResources(), new AlbumArtHelper.AlbumArtListener() {
            @Override
            public void onArtLoaded(RecyclingBitmapDrawable output, BoundEntity request) {
                try {
                    if (output != null && !isDetached()) {
                        mHeroImage = output.getBitmap();

                        if (materialTransition) {
                            MaterialTransitionDrawable mtd = new MaterialTransitionDrawable(
                                    (BitmapDrawable) getResources().getDrawable(R.drawable.ic_cloud_offline),
                                    (BitmapDrawable) getResources().getDrawable(R.drawable.album_placeholder));
                            mtd.transitionTo(output);

                            mHeroImageView.setImageDrawable(mtd);
                        } else {
                            final TransitionDrawable transition = new TransitionDrawable(
                                    new Drawable[] { mHeroImageView.getDrawable(), output });

                            // Make sure the transition happens after the activity animation is done,
                            // otherwise weird sliding occurs.
                            mHandler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    mHeroImageView.setImageDrawable(transition);
                                    transition.startTransition(500);
                                }
                            }, 600);
                        }

                        generateHeroPalette();
                    }
                } catch (IllegalStateException ignore) {
                    // We might have left the activity, so go on
                }
            }
        }, mArtist, -1, false);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onSongUpdate(List<Song> s) {
        boolean hasThisArtist = false;
        for (Song song : s) {
            if (mArtist.getRef().equals(song.getArtist())) {
                hasThisArtist = true;
                break;
            }
        }

        if (hasThisArtist) {
            // TODO: Instead of updating everything, update only the relevant entries
            mHandler.removeCallbacks(mUpdateAlbumsRunnable);
            mHandler.postDelayed(mUpdateAlbumsRunnable, 300);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onAlbumUpdate(List<Album> a) {
        boolean hasThisArtist = false;
        final ProviderAggregator aggregator = ProviderAggregator.getDefault();
        for (Album album : a) {
            Iterator<String> songs = album.songs();
            while (songs.hasNext()) {
                String songRef = songs.next();
                Song song = aggregator.retrieveSong(songRef, album.getProvider());
                if (song != null && mArtist != null && mArtist.getRef().equals(song.getArtist())) {
                    hasThisArtist = true;
                    break;
                }
            }

            if (hasThisArtist) {
                break;
            }
        }

        if (hasThisArtist) {
            // TODO: Instead of updating everything, update only the relevant entries
            mHandler.removeCallbacks(mUpdateAlbumsRunnable);
            mHandler.postDelayed(mUpdateAlbumsRunnable, 300);
        }
    }

    @Override
    public void onPlaylistUpdate(List<Playlist> p) {
    }

    @Override
    public void onPlaylistRemoved(String ref) {
    }

    @Override
    public void onArtistUpdate(final List<Artist> a) {
        if (a.contains(mArtist)) {
            mHandler.removeCallbacks(mUpdateAlbumsRunnable);
            mHandler.postDelayed(mUpdateAlbumsRunnable, 300);
        }
        mArtistSimilarFragment.notifyArtistUpdate(a);
    }

    @Override
    public void onProviderConnected(IMusicProvider provider) {
    }

    @Override
    public void onSearchResult(List<SearchResult> searchResult) {
    }

    /**
     * Class representing the inner fragment displaying albums and tracks
     */
    public static class ArtistTracksFragment extends Fragment {
        private Song mRecommendedSong;
        private boolean mRecommendationLoaded = false;
        private View mRootView;
        private ArtistFragment mParent;
        private HashMap<Song, View> mSongToViewMap = new HashMap<>();
        private HashMap<String, View> mAlbumToViewMap = new HashMap<>();
        private View mPreviousSongGroup;
        private View mPreviousAlbumGroup;
        private TextView mOfflineView;
        private Handler mHandler;

        private Comparator<Album> mComparator = new Comparator<Album>() {
            @Override
            public int compare(Album album, Album album2) {
                if (album.getYear() != album2.getYear()) {
                    return album.getYear() < album2.getYear() ? 1 : -1;
                } else if (album.getName() != null && album2.getName() != null) {
                    return album.getName().compareTo(album2.getName());
                } else {
                    return album.getRef().compareTo(album2.getRef());
                }
            }
        };

        private View.OnClickListener mPlayAlbumClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final ProviderAggregator aggregator = ProviderAggregator.getDefault();
                final AlbumViewHolder tag = (AlbumViewHolder) view.getTag();
                final Album album = tag.album;
                final PlayPauseDrawable drawable = (PlayPauseDrawable) tag.ivPlayAlbum.getDrawable();

                if (mPreviousAlbumGroup != null) {
                    ImageView ivPlayAlbum = (ImageView) mPreviousAlbumGroup.findViewById(R.id.ivPlayAlbum);
                    PlayPauseDrawable existingDrawable = (PlayPauseDrawable) ivPlayAlbum.getDrawable();
                    existingDrawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
                }

                mPreviousAlbumGroup = tag.vRoot;

                if (drawable.getRequestedShape() == PlayPauseDrawable.SHAPE_STOP) {
                    drawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
                    mParent.setFabShape(PlayPauseDrawable.SHAPE_PLAY);
                    PlaybackProxy.pause();
                } else {
                    drawable.setShape(PlayPauseDrawable.SHAPE_STOP);
                    mParent.setFabShape(PlayPauseDrawable.SHAPE_PAUSE);
                    PlaybackProxy.playAlbum(album);

                    // Bold the corresponding track
                    final Iterator<String> songs = album.songs();
                    if (songs.hasNext()) {
                        final String songRef = songs.next();
                        final Song s = aggregator.retrieveSong(songRef, album.getProvider());
                        boldPlayingTrack(s);
                    }
                }
            }
        };

        /**
         * View holder for items in this class
         */
        private class AlbumViewHolder {
            TextView tvAlbumName;
            TextView tvAlbumYear;
            AlbumArtImageView ivCover;
            ImageView ivPlayAlbum;
            Album album;
            View vRoot;

            AlbumViewHolder(View viewRoot) {
                vRoot = viewRoot;
                tvAlbumName = (TextView) viewRoot.findViewById(R.id.tvAlbumName);
                tvAlbumYear = (TextView) viewRoot.findViewById(R.id.tvAlbumYear);
                ivCover = (AlbumArtImageView) viewRoot.findViewById(R.id.ivCover);
                ivPlayAlbum = (ImageView) viewRoot.findViewById(R.id.ivPlayAlbum);
            }
        }

        private View.OnClickListener mSongClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final ProviderAggregator aggregator = ProviderAggregator.getDefault();

                final Song song = (Song) view.getTag();
                Album album = null;
                if (song.getAlbum() != null) {
                    album = aggregator.retrieveAlbum(song.getAlbum(), song.getProvider());
                }

                if (Utils.canPlaySong(song)) {
                    // Queue the full album, if possible
                    PlaybackProxy.clearQueue();
                    if (album != null) {
                        PlaybackProxy.queueAlbum(album, false);

                        // Find the clicked song index and play it
                        Iterator<String> songs = album.songs();
                        int i = 0;
                        while (songs.hasNext()) {
                            String songRef = songs.next();
                            if (songRef.equals(song.getRef())) {
                                break;
                            } else {
                                ++i;
                            }
                        }

                        PlaybackProxy.playAtIndex(i);
                    } else {
                        // We have no album information, just play the song
                        PlaybackProxy.playSong(song);
                    }

                    // Update UI
                    boldPlayingTrack(song);
                    updatePlayingAlbum(song.getAlbum());
                }
            }
        };

        /**
         * {@inheritDoc}
         */
        @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                @Nullable Bundle savedInstanceState) {
            mRootView = inflater.inflate(R.layout.fragment_artist_tracks, container, false);
            mHandler = new Handler();

            mOfflineView = (TextView) mRootView.findViewById(R.id.tvErrorMessage);
            mOfflineView.setText(R.string.error_artist_unavailable_offline);
            mOfflineView.setVisibility(View.GONE);

            return mRootView;
        }

        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);

            // Load recommendation and albums for the first
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    loadRecommendation();
                    loadAlbums(true);
                }
            });
        }

        /**
         * Sets the parent fragment
         *
         * @param parent The parent fragment
         */
        public void setParentFragment(ArtistFragment parent) {
            mParent = parent;
        }

        /**
         * Sets whether or not to show the loading spinner
         *
         * @param show True to show, false otherwise
         */
        private void showLoadingSpinner(final boolean show) {
            final ProgressBar pb = (ProgressBar) mRootView.findViewById(R.id.pbArtistLoading);
            if (show && pb.getVisibility() != View.VISIBLE || !show && pb.getVisibility() != View.GONE) {
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        pb.setVisibility(show ? View.VISIBLE : View.GONE);
                    }
                });
            }
        }

        /**
         * Play the artist radio
         */
        private void playRecommendation() {
            if (mRecommendationLoaded && mRecommendedSong != null) {
                // Generate radio tracks
                List<Song> tracks = Suggestor.getInstance().buildArtistRadio(mParent.getArtist());
                PlaybackProxy.clearQueue();

                // Add the recommended song itself first, then the generated tracks
                PlaybackProxy.queueSong(mRecommendedSong, true);

                for (Song song : tracks) {
                    PlaybackProxy.queueSong(song, false);
                }

                // And play!
                PlaybackProxy.playAtIndex(0);
                mParent.setFabShape(PlayPauseDrawable.SHAPE_PAUSE);
                mParent.setFabShouldResume(true);

                // Update UI indicators
                boldPlayingTrack(mRecommendedSong);
                updatePlayingAlbum(mRecommendedSong.getAlbum());
            }
        }

        /**
         * Load the recommended track
         */
        private synchronized void loadRecommendation() {
            if (mRecommendationLoaded || mParent == null || mRootView == null) {
                // Already loaded or parent not loaded yet or view not loaded yet
                return;
            }

            final ProviderAggregator aggregator = ProviderAggregator.getDefault();

            // Load a song from the Suggestor and display it
            Song recommended = Suggestor.getInstance().suggestBestForArtist(mParent.getArtist());
            if (recommended != null) {
                mRecommendedSong = recommended;
                Album album = aggregator.retrieveAlbum(recommended.getAlbum(), recommended.getProvider());

                CardView cvRec = (CardView) mRootView.findViewById(R.id.cardArtistSuggestion);
                TextView tvTitle = (TextView) mRootView.findViewById(R.id.tvArtistSuggestionTitle);
                TextView tvArtist = (TextView) mRootView.findViewById(R.id.tvArtistSuggestionArtist);
                Button btnPlayNow = (Button) mRootView.findViewById(R.id.btnArtistSuggestionPlay);
                tvTitle.setText(recommended.getTitle());

                if (album != null) {
                    tvArtist.setText(getString(R.string.from_the_album, album.getName()));
                } else {
                    tvArtist.setText("");
                }

                AlbumArtImageView ivCov = (AlbumArtImageView) mRootView.findViewById(R.id.ivArtistSuggestionCover);
                ivCov.setOnArtLoadedListener(new AlbumArtLoadListener(cvRec));
                if (recommended.getAlbum() != null) {
                    ivCov.loadArtForAlbum(
                            aggregator.retrieveAlbum(recommended.getAlbum(), recommended.getProvider()));
                }

                // If we were gone, animate in
                if (cvRec.getVisibility() == View.GONE) {
                    cvRec.setVisibility(View.VISIBLE);
                    cvRec.setAlpha(0.0f);
                    cvRec.animate().alpha(1.0f).setDuration(ANIMATION_DURATION).setInterpolator(mInterpolator)
                            .start();

                    View suggestionTitle = mRootView.findViewById(R.id.tvArtistSuggestionNote);
                    suggestionTitle.setVisibility(View.VISIBLE);
                    suggestionTitle.setAlpha(0.0f);
                    suggestionTitle.animate().alpha(1.0f).setDuration(ANIMATION_DURATION)
                            .setInterpolator(mInterpolator).start();
                }

                btnPlayNow.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        playRecommendation();
                    }
                });

                mRecommendationLoaded = true;
            } else {
                mRootView.findViewById(R.id.cardArtistSuggestion).setVisibility(View.GONE);
                mRootView.findViewById(R.id.tvArtistSuggestionNote).setVisibility(View.GONE);
                mRecommendationLoaded = false;
            }
        }

        /**
         * Displays a toast
         *
         * @param string A string resource of the text to display
         */
        private void postToast(@StringRes final int string) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    Utils.shortToast(getActivity(), string);
                }
            });
        }

        /**
         * Fetch the albums from the provider
         */
        private void fetchAlbums() {
            new Thread() {
                public void run() {
                    ProviderIdentifier pi = mParent.getArtist().getProvider();
                    ProviderConnection pc = PluginsLookup.getDefault().getProvider(pi);
                    if (pc != null) {
                        IMusicProvider provider = pc.getBinder();
                        if (provider != null) {
                            try {
                                boolean hasMore = provider.fetchArtistAlbums(mParent.getArtist().getRef());
                                showLoadingSpinner(hasMore && !ProviderAggregator.getDefault().isOfflineMode());
                                mParent.showFab(true, !hasMore);
                            } catch (RemoteException e) {
                                Log.e(TAG, "Unable to fetch artist albums", e);
                                postToast(R.string.plugin_error);
                            }
                        } else {
                            showLoadingSpinner(false);
                            mParent.showFab(true, true);
                            Log.e(TAG, "Provider is null, cannot fetch albums");
                            postToast(R.string.plugin_error);
                        }
                    } else {
                        showLoadingSpinner(false);
                        mParent.showFab(true, true);
                        Log.e(TAG, "ProviderConnection is null, cannot fetch albums");
                        postToast(R.string.plugin_error);
                    }
                }
            }.start();
        }

        /**
         * Load and display albums
         *
         * @param request Whether or not to fetch albums from the provider
         */
        private void loadAlbums(final boolean request) {
            if (request) {
                // Make sure we loaded all the albums for that artist
                fetchAlbums();
            }

            if (mRootView == null && mHandler != null) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        loadAlbums(request);
                    }
                }, 200);
                return;
            }

            // Check if we're offline, and if we have nothing to show, then show the offline error
            final ProviderAggregator aggregator = ProviderAggregator.getDefault();
            final LinearLayout llAlbums = (LinearLayout) mRootView.findViewById(R.id.llAlbums);
            llAlbums.removeAllViews();

            Iterator<String> albumIt = new ArrayList<>(mParent.getArtist().getAlbums()).iterator();
            List<Album> albums = new ArrayList<>();

            if (aggregator.isOfflineMode() && !albumIt.hasNext()) {
                mOfflineView.setVisibility(View.VISIBLE);
                mRootView.findViewById(R.id.tvHeaderAlbums).setVisibility(View.GONE);
            } else {
                mOfflineView.setVisibility(View.GONE);
                mRootView.findViewById(R.id.tvHeaderAlbums).setVisibility(View.VISIBLE);

                while (albumIt.hasNext()) {
                    String albumRef = albumIt.next();
                    Album album = aggregator.retrieveAlbum(albumRef, mParent.getArtist().getProvider());

                    if (album != null) {
                        ProviderConnection conn = PluginsLookup.getDefault().getProvider(album.getProvider());
                        if (conn != null) {
                            IMusicProvider provider = conn.getBinder();
                            try {
                                if (provider != null) {
                                    provider.fetchAlbumTracks(albumRef);
                                }
                            } catch (RemoteException e) {
                                Log.e(TAG, "Remote exception while trying to fetch album tracks", e);
                            }
                            albums.add(album);
                        }
                    }
                }
            }

            // Sort it from album names
            try {
                Collections.sort(albums, mComparator);
            } catch (IllegalArgumentException ignore) {
            }

            // Then inflate views
            final LayoutInflater inflater = getActivity().getLayoutInflater();
            for (final Album album : albums) {
                AlbumViewHolder holder;
                View viewRoot;
                if (mAlbumToViewMap.containsKey(album.getRef())) {
                    viewRoot = mAlbumToViewMap.get(album.getRef());
                    holder = (AlbumViewHolder) viewRoot.getTag();
                    holder.album = album;
                    llAlbums.addView(viewRoot);

                    AlbumGroupClickListener listener = new AlbumGroupClickListener(album, mParent.mRootView,
                            llAlbums, viewRoot, this);
                    viewRoot.setOnClickListener(listener);
                } else {
                    viewRoot = inflater.inflate(R.layout.exp_group_albums, llAlbums, false);
                    llAlbums.addView(viewRoot);

                    holder = new AlbumViewHolder(viewRoot);
                    holder.album = album;
                    viewRoot.setTag(holder);
                    holder.ivPlayAlbum.setTag(holder);

                    AlbumGroupClickListener listener = new AlbumGroupClickListener(album, mParent.mRootView,
                            llAlbums, viewRoot, this);
                    viewRoot.setOnClickListener(listener);

                    final PlayPauseDrawable drawable = new PlayPauseDrawable(getResources(), 1);
                    drawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
                    drawable.setColor(0xCC333333);
                    holder.ivPlayAlbum.setImageDrawable(drawable);
                    holder.ivPlayAlbum.setOnClickListener(mPlayAlbumClickListener);
                }

                // If the album is loaded, show its metadata
                if (album.isLoaded()) {
                    holder.tvAlbumName.setText(album.getName());
                    if (album.getYear() > 0) {
                        holder.tvAlbumYear.setVisibility(View.VISIBLE);
                        holder.tvAlbumYear.setText(Integer.toString(album.getYear()));
                    } else {
                        holder.tvAlbumYear.setVisibility(View.GONE);
                    }

                    holder.ivPlayAlbum.setVisibility(View.VISIBLE);

                    // Set play or pause based on if this album is playing
                    int state = PlaybackProxy.getState();
                    if (state == PlaybackService.STATE_PLAYING) {
                        Song currentSong = PlaybackProxy.getCurrentTrack();
                        if (currentSong != null && album.getRef().equals(currentSong.getAlbum())) {
                            updatePlayingAlbum(currentSong.getAlbum());
                        }
                    }
                } else {
                    // Album isn't loaded, show "Loading"
                    holder.tvAlbumName.setText(getString(R.string.loading));
                    holder.tvAlbumYear.setVisibility(View.GONE);
                    holder.ivPlayAlbum.setVisibility(View.GONE);
                }

                // Load the album art
                holder.ivCover.loadArtForAlbum(album);

                // Cache the view
                mAlbumToViewMap.put(album.getRef(), viewRoot);
            }

            // Hide loading spinner and show the play FAB as we now have stuff
            showLoadingSpinner(false);
            mParent.showFab(true, true);
        }

        /**
         * Shows the album tracks of the provided album
         *
         * @param album     The album of which display tracks
         * @param container The container in which expand views
         */
        private void showAlbumTracks(Album album, LinearLayout container) {
            Iterator<String> songsIt = album.songs();

            final LayoutInflater inflater = getActivity().getLayoutInflater();
            final ProviderAggregator aggregator = ProviderAggregator.getDefault();

            while (songsIt.hasNext()) {
                final Song song = aggregator.retrieveSong(songsIt.next(), album.getProvider());
                if (song == null) {
                    Log.w(TAG, "Null song in album!");
                    continue;
                }

                View itemRoot = inflater.inflate(R.layout.exp_item_albums, container, false);
                container.addView(itemRoot);
                itemRoot.setTag(song);

                // Set alpha based on offline availability and mode
                if ((aggregator.isOfflineMode() && (song.getOfflineStatus() != BoundEntity.OFFLINE_STATUS_READY)
                        || (!song.isAvailable()))) {
                    Utils.setChildrenAlpha((ViewGroup) itemRoot,
                            Float.parseFloat(getString(R.string.unavailable_track_alpha)));
                } else {
                    Utils.setChildrenAlpha((ViewGroup) itemRoot, 1.0f);
                }

                mSongToViewMap.put(song, itemRoot);

                TextView tvTrackName = (TextView) itemRoot.findViewById(R.id.tvTrackName);
                TextView tvTrackDuration = (TextView) itemRoot.findViewById(R.id.tvTrackDuration);
                final ImageView ivOverflow = (ImageView) itemRoot.findViewById(R.id.ivOverflow);
                final ImageView ivOffline = (ImageView) itemRoot.findViewById(R.id.ivOffline);
                ivOffline.setVisibility(View.VISIBLE);

                switch (song.getOfflineStatus()) {
                case BoundEntity.OFFLINE_STATUS_DOWNLOADING:
                    ivOffline.setImageResource(R.drawable.ic_sync_in_progress);
                    break;

                case BoundEntity.OFFLINE_STATUS_ERROR:
                    ivOffline.setImageResource(R.drawable.ic_sync_problem);
                    break;

                case BoundEntity.OFFLINE_STATUS_NO:
                    ivOffline.setVisibility(View.GONE);
                    break;

                case BoundEntity.OFFLINE_STATUS_READY:
                    ivOffline.setImageResource(R.drawable.ic_track_downloaded);
                    break;

                case BoundEntity.OFFLINE_STATUS_PENDING:
                    ivOffline.setImageResource(R.drawable.ic_track_download_pending);
                    break;
                }

                if (song.isLoaded()) {
                    tvTrackName.setText(song.getTitle());
                    tvTrackDuration.setText(Utils.formatTrackLength(song.getDuration()));
                    ivOverflow.setVisibility(View.VISIBLE);

                    // Set song click listener if playable
                    if (song.isAvailable()) {
                        itemRoot.setOnClickListener(mSongClickListener);
                    }

                    // Set overflow popup
                    ivOverflow.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            Utils.showSongOverflow(getActivity(), ivOverflow, song, true);
                        }
                    });

                    // Bold if already playing
                    int state = PlaybackProxy.getState();
                    if (state == PlaybackService.STATE_PLAYING) {
                        Song currentSong = PlaybackProxy.getCurrentTrack();
                        if (currentSong != null && song.getRef().equals(currentSong.getRef())) {
                            boldPlayingTrack(currentSong);
                        }
                    }
                } else {
                    tvTrackName.setText(getString(R.string.loading));
                    tvTrackDuration.setText("");
                    ivOverflow.setVisibility(View.GONE);
                }
            }
        }

        /**
         * Updates the current playing album
         *
         * @param albumRef The new album being played
         */
        private void updatePlayingAlbum(String albumRef) {
            View view = mAlbumToViewMap.get(albumRef);
            ImageView ivPlayAlbum;

            if (mPreviousAlbumGroup != null) {
                ivPlayAlbum = (ImageView) mPreviousAlbumGroup.findViewById(R.id.ivPlayAlbum);
                PlayPauseDrawable drawable = (PlayPauseDrawable) ivPlayAlbum.getDrawable();
                drawable.setShape(PlayPauseDrawable.SHAPE_PLAY);
            }

            if (view != null) {
                ivPlayAlbum = (ImageView) view.findViewById(R.id.ivPlayAlbum);
                PlayPauseDrawable drawable = (PlayPauseDrawable) ivPlayAlbum.getDrawable();
                drawable.setShape(PlayPauseDrawable.SHAPE_STOP);
            }

            mPreviousAlbumGroup = view;
        }

        /**
         * Finds and bolds the track that is currently playing, if we have it
         *
         * @param s The song that is currently playing
         */
        private void boldPlayingTrack(Song s) {
            View view = mSongToViewMap.get(s);

            TextView tvTrackName, tvTrackDuration;

            if (mPreviousSongGroup != null) {
                tvTrackName = (TextView) mPreviousSongGroup.findViewById(R.id.tvTrackName);
                tvTrackDuration = (TextView) mPreviousSongGroup.findViewById(R.id.tvTrackDuration);
                tvTrackName.setTypeface(null, Typeface.NORMAL);
                tvTrackDuration.setTypeface(null, Typeface.NORMAL);
            }

            if (view != null) {
                tvTrackName = (TextView) view.findViewById(R.id.tvTrackName);
                tvTrackDuration = (TextView) view.findViewById(R.id.tvTrackDuration);
                tvTrackName.setTypeface(null, Typeface.BOLD);
                tvTrackDuration.setTypeface(null, Typeface.BOLD);
            }

            mPreviousSongGroup = view;
        }
    }

    /**
     * Fragment showing the Artist's biography
     */
    public static class ArtistInfoFragment extends Fragment {
        private static Artist mArtist;
        private Handler mHandler;
        private TextView mArtistInfo;
        private TextView mOfflineView;
        private boolean mInfoLoaded;
        private ProgressBar mLoadingSpinner;

        /**
         * Default constructor
         */
        public ArtistInfoFragment() {
            mHandler = new Handler();
            mInfoLoaded = false;
        }

        /**
         * Sets the active artist for which get the biography
         *
         * @param artist The artist displayed
         */
        public void setArguments(Artist artist) {
            mArtist = artist;
        }

        /**
         * Notifies that the fragment is currently active, and that data should be populated.
         */
        public void notifyActive() {
            if (!mInfoLoaded) {
                if (ProviderAggregator.getDefault().isOfflineMode()) {
                    mOfflineView.setVisibility(View.VISIBLE);
                    mLoadingSpinner.setVisibility(View.GONE);
                } else {
                    mOfflineView.setVisibility(View.GONE);
                    mLoadingSpinner.setVisibility(View.VISIBLE);
                    mInfoLoaded = true;
                    new Thread() {
                        public void run() {
                            loadBiographySync();
                        }
                    }.start();
                }
            }
        }

        /**
         * Loads synchronously the biography data
         */
        private void loadBiographySync() {
            final EchoNest echoNest = new EchoNest();
            try {
                com.echonest.api.v4.Artist enArtist = echoNest.searchArtistByName(mArtist.getName());
                if (enArtist != null) {
                    final Biography bio = echoNest.getArtistBiography(enArtist);
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (mLoadingSpinner != null && mArtistInfo != null && !isDetached()) {
                                mLoadingSpinner.setVisibility(View.GONE);

                                if (bio != null) {
                                    mArtistInfo.setText(getString(R.string.biography_format, bio.getText(),
                                            bio.getSite(), bio.getURL(), bio.getLicenseType(),
                                            bio.getLicenseAttribution()));
                                } else {
                                    mArtistInfo.setText(getString(R.string.no_bio_available));
                                }
                            }
                        }
                    });
                }
            } catch (Exception e) {
                Log.e(TAG, "Unable to get artist information", e);
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        Utils.shortToast(getActivity(), R.string.unable_fetch_artist_info);
                    }
                });
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                @Nullable Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_artist_info, container, false);
            mArtistInfo = (TextView) rootView.findViewById(R.id.tvArtistInfo);
            mLoadingSpinner = (ProgressBar) rootView.findViewById(R.id.pbArtistInfo);
            mOfflineView = (TextView) rootView.findViewById(R.id.tvErrorMessage);
            mOfflineView.setText(R.string.error_biography_unavailable_offline);
            return rootView;
        }
    }

    /**
     * Fragment showing similar artists
     */
    public static class ArtistSimilarFragment extends Fragment {
        private RecyclerView mArtistsGrid;
        private Artist mArtist;
        private boolean mSimilarLoaded;
        private ArtistsAdapter mAdapter;
        private Handler mHandler;
        private List<Artist> mSimilarArtists;
        private ProgressBar mArtistsSpinner;
        private TextView mOfflineView;

        /**
         * Default constructor
         */
        public ArtistSimilarFragment() {
            mAdapter = new ArtistsAdapter();
            mHandler = new Handler();
            mSimilarArtists = new ArrayList<>();
        }

        /**
         * Sets the active artist to display
         *
         * @param artist The artist displayed
         */
        public void setArguments(Artist artist) {
            mArtist = artist;
        }

        /**
         * Notify that the fragment is active and that data should be populated
         */
        public void notifyActive() {
            Log.e(TAG, "Notify Active Similar, similarLoaded=" + mSimilarLoaded);
            if (!mSimilarLoaded) {
                if (ProviderAggregator.getDefault().isOfflineMode()) {
                    if (mOfflineView != null)
                        mOfflineView.setVisibility(View.VISIBLE);
                    if (mArtistsSpinner != null)
                        mArtistsSpinner.setVisibility(View.GONE);
                } else {
                    if (mOfflineView != null)
                        mOfflineView.setVisibility(View.GONE);
                    if (mArtistsSpinner != null)
                        mArtistsSpinner.setVisibility(View.VISIBLE);
                    mSimilarLoaded = true;
                    new Thread() {
                        public void run() {
                            loadSimilarSync();
                        }
                    }.start();
                }
            } else {
                ensureSimilar();
            }
        }

        /**
         * Called when the host fragment notifies that artists have been updated by a provider
         *
         * @param artists The artists who got updated
         */
        public void notifyArtistUpdate(final List<Artist> artists) {
            for (Artist artist : artists) {
                final int artistIndex = mSimilarArtists.indexOf(artist);
                if (artistIndex >= 0) {
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            mAdapter.notifyDataSetChanged();
                        }
                    });
                }
            }
        }

        /**
         * Load synchronously the similar artists
         */
        public void loadSimilarSync() {
            EchoNest echoNest = new EchoNest();
            try {
                com.echonest.api.v4.Artist enArtist = echoNest.searchArtistByName(mArtist.getName());
                if (enArtist != null) {
                    List<com.echonest.api.v4.Artist> similars = echoNest.getArtistSimilar(enArtist);

                    // Retrieve the rosetta stone prefix
                    String rosettaPreferred = ProviderAggregator.getDefault().getPreferredRosettaStonePrefix();
                    ProviderIdentifier rosettaProvider = null;
                    if (rosettaPreferred != null) {
                        rosettaProvider = ProviderAggregator.getDefault()
                                .getRosettaStoneIdentifier(rosettaPreferred);
                    }

                    // For each similar artist, get the rosetta stone ID, and add it to the adapter
                    for (com.echonest.api.v4.Artist similar : similars) {
                        if (rosettaPreferred != null) {
                            String ref = echoNest.getArtistForeignID(similar, rosettaPreferred);
                            if (ref != null) {
                                Artist artist = ProviderAggregator.getDefault().retrieveArtist(ref,
                                        rosettaProvider);
                                if (artist != null) {
                                    mSimilarArtists.add(artist);
                                } else {
                                    Log.e(TAG, "Null artist for similar");
                                }
                            }
                        }
                    }

                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            ensureSimilar();
                        }
                    });
                }
            } catch (EchoNestException e) {
                Log.e(TAG, "Cannot get similar artist", e);
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mOfflineView.setVisibility(View.VISIBLE);
                    }
                });

            }
        }

        private void ensureSimilar() {
            mAdapter.addAllUnique(mSimilarArtists);
            mAdapter.notifyDataSetChanged();

            if (mArtistsGrid != null && mArtistsSpinner != null) {
                mArtistsGrid.setAdapter(mAdapter);
                mArtistsSpinner.setVisibility(View.GONE);
                mArtistsGrid.setVisibility(View.VISIBLE);
                mOfflineView.setVisibility(View.GONE);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                @Nullable Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_artist_similar, container, false);
        }

        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            View rootView = getView();
            if (rootView != null) {
                mArtistsSpinner = (ProgressBar) rootView.findViewById(R.id.pbSimilarArtists);

                mArtistsGrid = (RecyclerView) rootView.findViewById(R.id.gridSimilarArtists);
                mArtistsGrid.setHasFixedSize(true);
                mArtistsGrid.setLayoutManager(new GridLayoutManager(view.getContext(), 2));
                mOfflineView = (TextView) rootView.findViewById(R.id.tvErrorMessage);
                mOfflineView.setText(R.string.error_similar_unavailable_offline);
            }
        }
    }
}