org.opensilk.music.artwork.ArtworkRequestManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.opensilk.music.artwork.ArtworkRequestManagerImpl.java

Source

/*
 * Copyright (c) 2014 OpenSilk Productions 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 org.opensilk.music.artwork;

import android.content.Context;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;

import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.google.gson.Gson;
import com.jakewharton.disklrucache.DiskLruCache;

import org.apache.commons.io.IOUtils;
import org.opensilk.common.dagger.qualifier.ForApplication;
import org.opensilk.common.widget.AnimatedImageView;
import org.opensilk.music.AppPreferences;
import org.opensilk.music.api.meta.ArtInfo;
import org.opensilk.music.artwork.cache.ArtworkCache;
import org.opensilk.music.artwork.cache.ArtworkLruCache;
import org.opensilk.music.artwork.cache.BitmapCache;
import org.opensilk.music.artwork.cache.BitmapDiskCache;
import org.opensilk.music.artwork.cache.BitmapDiskLruCache;
import org.opensilk.music.ui2.loader.AlbumArtInfoLoader;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import de.umass.lastfm.Album;
import de.umass.lastfm.Artist;
import de.umass.lastfm.ImageSize;
import de.umass.lastfm.MusicEntry;
import de.umass.lastfm.opensilk.Fetch;
import de.umass.lastfm.opensilk.MusicEntryResponseCallback;
import hugo.weaving.DebugLog;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import timber.log.Timber;

/**
 * Created by drew on 10/21/14.
 */
@Singleton
public class ArtworkRequestManagerImpl implements ArtworkRequestManager {
    final static boolean DROP_CRUMBS = false;

    final Context mContext;
    final AppPreferences mPreferences;
    final ArtworkCache mL1Cache;
    final BitmapDiskCache mL2Cache;
    final RequestQueue mVolleyQueue;
    final Gson mGson;
    final ConnectivityManager mConnectivityManager;

    final Map<RequestKey, IArtworkRequest> mActiveRequests = new LinkedHashMap<>(10);

    @Inject
    public ArtworkRequestManagerImpl(@ForApplication Context mContext, AppPreferences mPreferences,
            @Named("L1Cache") ArtworkCache mL1Cache, @Named("L2Cache") BitmapDiskCache mL2Cache,
            RequestQueue mVolleyQueue, Gson mGson, ConnectivityManager mConnectivityManager) {
        this.mContext = mContext;
        this.mPreferences = mPreferences;
        this.mL1Cache = mL1Cache;
        this.mL2Cache = mL2Cache;
        this.mVolleyQueue = mVolleyQueue;
        this.mGson = mGson;
        this.mConnectivityManager = mConnectivityManager;
    }

    static class CrumbTrail {
        final ArrayList<String> breadcrumbs = new ArrayList<>();
        boolean followed = false;

        void drop(String msg) {
            breadcrumbs.add(msg);
        }

        void follow() {
            followed = true;
            Timber.v(assembleTrail());
        }

        private String assembleTrail() {
            StringBuilder b = new StringBuilder(500);
            for (int ii = 0; ii < breadcrumbs.size(); ii++) {
                b.append(breadcrumbs.get(ii));
                if (ii < breadcrumbs.size() - 1)
                    b.append(" -> ");
            }
            return b.toString();
        }

        @Override
        protected void finalize() throws Throwable {
            if (!followed) {
                Timber.e("CrumbTrail never followed: %s", assembleTrail());
            }
            super.finalize();
        }
    }

    interface IArtworkRequest {
        void addRecipient(ImageContainer c);
    }

    /**
     * TODO hook ImageContainer to unsubscribe request when all containers
     * TODO have unsubscribed, and find way to cancel volley requests when unsubscribed
     */
    abstract class BaseArtworkRequest implements Subscription, IArtworkRequest {
        final RequestKey key;
        final ArtInfo artInfo;
        final ArtworkType artworkType;

        final List<ImageContainer> recipients = new LinkedList<>();

        Subscription subscription;
        boolean unsubscribed = false;
        boolean inflight = false;
        boolean complete = false;

        BaseArtworkRequest(RequestKey key) {
            this.key = key;
            this.artInfo = key.artInfo;
            this.artworkType = key.artworkType;
            addBreadcrumb(getCacheKey(artInfo, artworkType));
        }

        @Override
        public void unsubscribe() {
            addBreadcrumb("unsubscribe");
            unsubscribed = true;
            if (subscription != null) {
                subscription.unsubscribe();
                subscription = null;
            }
            if (artInfo != null) {
                mVolleyQueue.cancelAll(artInfo);
            }
            onComplete();
        }

        @Override
        public boolean isUnsubscribed() {
            return unsubscribed;
        }

        @Override
        public void addRecipient(ImageContainer c) {
            if (complete) {
                mActiveRequests.remove(key);
                throw new IllegalStateException("Tried to add recipient after complete");
            }
            recipients.add(c);
            if (!inflight) {
                inflight = true;
                start();
            } else {
                c.setDefaultImage();
            }
        }

        void start() {
            addBreadcrumb("start");
            tryForCache();
        }

        void onComplete() {
            addBreadcrumb("complete");
            complete = true;
            mActiveRequests.remove(key);
            printTrail();
        }

        void setDefaultImage() {
            addBreadcrumb("setDefaultImage");
            if (unsubscribed)
                return;
            for (ImageContainer c : recipients) {
                if (c.isUnsubscribed())
                    continue;
                c.setDefaultImage();
            }
        }

        void onResponse(Artwork artwork, boolean fromCache, boolean shouldAnimate) {
            addBreadcrumb("onResponse(" + fromCache + ")");
            if (unsubscribed)
                return;
            for (ImageContainer c : recipients) {
                if (c.isUnsubscribed())
                    continue;
                c.setImageBitmap(artwork.bitmap, shouldAnimate);
                c.notifyPaletteObserver(artwork.palette, shouldAnimate);
            }
        }

        void tryForCache() {
            addBreadcrumb("tryForCache");
            if (!validateArtInfo()) {
                addBreadcrumb("malformedArtInfo");
                setDefaultImage();
                onComplete();
                return;
            }
            subscription = createCacheObservable(artInfo, artworkType).subscribe(new Action1<CacheResponse>() {
                @Override
                public void call(CacheResponse cr) {
                    addBreadcrumb("tryForCache hit");
                    onResponse(cr.artwork, true, !cr.fromL1);
                    onComplete();
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    addBreadcrumb("tryForCache miss");
                    if (throwable instanceof CacheMissException) {
                        onCacheMiss();
                    } else {
                        setDefaultImage();
                        onComplete();
                    }
                }
            });
        }

        abstract boolean validateArtInfo();

        abstract void onCacheMiss();

        CrumbTrail crumbTrail;

        void addBreadcrumb(String crumb) {
            if (!DROP_CRUMBS)
                return;
            if (crumbTrail == null)
                crumbTrail = new CrumbTrail();
            crumbTrail.drop(crumb);
        }

        void printTrail() {
            if (!DROP_CRUMBS)
                return;
            crumbTrail.follow();
        }
    }

    class ArtistArtworkRequest extends BaseArtworkRequest {

        ArtistArtworkRequest(RequestKey key) {
            super(key);
        }

        @Override
        boolean validateArtInfo() {
            return !TextUtils.isEmpty(artInfo.artistName);
        }

        @Override
        void onCacheMiss() {
            addBreadcrumb("onCacheMiss");
            setDefaultImage();
            boolean isOnline = isOnline(mPreferences.getBoolean(AppPreferences.ONLY_ON_WIFI, true));
            boolean wantArtistImages = mPreferences.getBoolean(AppPreferences.DOWNLOAD_MISSING_ARTIST_IMAGES, true);
            if (isOnline && wantArtistImages) {
                addBreadcrumb("goingForNetwork");
                tryForNetwork();
            } else {
                onComplete();
            }
        }

        void tryForNetwork() {
            subscription = createArtistNetworkRequest(artInfo, artworkType).subscribe(new Action1<Artwork>() {
                @Override
                public void call(Artwork artwork) {
                    addBreadcrumb("tryForNetwork hit");
                    onResponse(artwork, false, true);
                    onComplete();
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    addBreadcrumb("tryForNetwork miss");
                    //                            Timber.w(throwable, "Unable to obtain image for %s", artInfo);
                    onComplete();
                }
            });
        }
    }

    class AlbumArtworkRequest extends BaseArtworkRequest {

        AlbumArtworkRequest(RequestKey key) {
            super(key);
        }

        @Override
        boolean validateArtInfo() {
            return (!TextUtils.isEmpty(artInfo.artistName) && !TextUtils.isEmpty(artInfo.albumName))
                    || (artInfo.artworkUri != null && !artInfo.artworkUri.equals(Uri.EMPTY));
        }

        @Override
        void onCacheMiss() {
            addBreadcrumb("onCacheMiss");
            setDefaultImage();
            //check if we have everything we need to download artwork
            boolean hasAlbumArtist = !TextUtils.isEmpty(artInfo.albumName)
                    && !TextUtils.isEmpty(artInfo.artistName);
            boolean hasUri = artInfo.artworkUri != null && !artInfo.artworkUri.equals(Uri.EMPTY);
            boolean isOnline = isOnline(mPreferences.getBoolean(AppPreferences.ONLY_ON_WIFI, true));
            boolean wantAlbumArt = mPreferences.getBoolean(AppPreferences.DOWNLOAD_MISSING_ARTWORK, true);
            boolean preferDownload = mPreferences.getBoolean(AppPreferences.PREFER_DOWNLOAD_ARTWORK, false);
            boolean isLocalArt = isLocalArtwork(artInfo.artworkUri);
            if (hasAlbumArtist && hasUri) {
                addBreadcrumb("hasAlbumArtist && hasUri");
                // We have everything we may need
                if (isOnline && wantAlbumArt) {
                    addBreadcrumb("isOnline && wantAlbumArt");
                    // were online and want artwork
                    if (isLocalArt && !preferDownload) {
                        addBreadcrumb("goingForMediaStore(true)");
                        // try mediastore first if local and user prefers
                        tryForMediaStore(true);
                    } else if (!isLocalArt && !preferDownload) {
                        // remote art and dont want to try for lfm
                        addBreadcrumb("goingForUrl");
                        tryForUrl();
                    } else {
                        addBreadcrumb("goingForNetwork(true)");
                        // go to network, falling back on fail
                        tryForNetwork(true);
                    }
                } else if (isOnline && !isLocalArt) {
                    addBreadcrumb("goingForUrl");
                    // were online and have an external uri lets get it
                    // regardless of user preference
                    tryForUrl();
                } else if (!isOnline && isLocalArt && !preferDownload) {
                    addBreadcrumb("goingForMediaStore(false)");
                    // were offline, this is a local source
                    // and the user doesnt want to try network first
                    // go ahead and fetch the mediastore image
                    tryForMediaStore(false);
                } else {
                    //  were offline and cant get artwork or the user wants to defer
                    addBreadcrumb("defer fetching art");
                    onComplete();
                }
            } else if (hasAlbumArtist) {
                addBreadcrumb("hasAlbumArtist");
                if (isOnline) {
                    addBreadcrumb("tryForNetwork(false)");
                    // try for network, we dont have a uri so dont fallback on failure
                    tryForNetwork(false);
                } else {
                    onComplete();
                }
            } else if (hasUri) {
                addBreadcrumb("hasUri");
                if (isLocalArt) {
                    addBreadcrumb("goingForMediaStore(false)");
                    //Wait what? this should never happen
                    tryForMediaStore(false);
                } else if (isOnline(false)) { //ignore wifi only request for remote urls
                    addBreadcrumb("goingForUrl");
                    //all we have is a url so go for it
                    tryForUrl();
                } else {
                    onComplete();
                }
            } else { // just ignore the request
                onComplete();
            }
        }

        void tryForNetwork(final boolean tryFallbackOnFail) {
            subscription = createAlbumNetworkObservable(artInfo, artworkType).subscribe(new Action1<Artwork>() {
                @Override
                public void call(Artwork artwork) {
                    addBreadcrumb("tryForNetwork hit");
                    onResponse(artwork, false, true);
                    onComplete();
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    addBreadcrumb("tryForNetwork miss");
                    onNetworkMiss(tryFallbackOnFail);
                }
            });
        }

        void onNetworkMiss(final boolean tryFallback) {
            addBreadcrumb("onNetworkMiss");
            boolean isLocalArt = isLocalArtwork(artInfo.artworkUri);
            if (tryFallback) {
                if (isLocalArt) {
                    addBreadcrumb("goingForMediaStore(false)");
                    tryForMediaStore(false);
                } else {
                    addBreadcrumb("goingForUrl");
                    tryForUrl();
                }
            } else {
                onComplete();
            }
        }

        void tryForMediaStore(final boolean tryNetworkOnFailure) {
            subscription = createMediaStoreRequestObservable(artInfo, artworkType)
                    .subscribe(new Action1<Artwork>() {
                        @Override
                        public void call(Artwork artwork) {
                            addBreadcrumb("tryForMediaStore hit");
                            onResponse(artwork, false, true);
                            onComplete();
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            addBreadcrumb("tryForMediaStore miss");
                            onMediaStoreMiss(tryNetworkOnFailure);
                        }
                    });
        }

        void onMediaStoreMiss(boolean tryNetwork) {
            addBreadcrumb("onMediaStoreMiss");
            if (tryNetwork) {
                addBreadcrumb("goingForNetwork");
                tryForNetwork(false);
            } else {
                onComplete();
            }
        }

        void tryForUrl() {
            subscription = createImageRequestObservable(artInfo.artworkUri.toString(), artInfo, artworkType)
                    .subscribe(new Action1<Artwork>() {
                        @Override
                        public void call(Artwork artwork) {
                            addBreadcrumb("tryForUrl hit");
                            onResponse(artwork, false, true);
                            onComplete();
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            addBreadcrumb("tryForUrl miss");
                            //                            Timber.w(throwable, "tryForUrl %s", artInfo);
                            onComplete();
                        }
                    });
        }
    }

    class WrappedImageContainer implements Subscription {

        final ImageContainer container;

        Subscription subscription;

        WrappedImageContainer(AnimatedImageView imageView, PaletteObserver paletteObserver, long albumId,
                ArtworkType artworkType) {
            this.container = new ImageContainer(imageView, paletteObserver);
            getArtInfo(albumId, artworkType);
        }

        @Override
        public void unsubscribe() {
            container.unsubscribe();
            if (subscription != null) {
                subscription.unsubscribe();
            }
        }

        @Override
        public boolean isUnsubscribed() {
            return container.isUnsubscribed();
        }

        void getArtInfo(final long albumId, final ArtworkType artworkType) {
            subscription = new AlbumArtInfoLoader(mContext, new long[] { albumId }).createObservable()
                    .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Action1<ArtInfo>() {
                        @Override
                        public void call(ArtInfo artInfo) {
                            RequestKey k = new RequestKey(artInfo, artworkType);
                            queueRequest(container, k, true);
                        }
                    }, new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            container.setDefaultImage();
                        }
                    });
        }
    }

    /*
     * Start IMPL
     */

    @Override
    public Subscription newAlbumRequest(AnimatedImageView imageView, PaletteObserver paletteObserver,
            ArtInfo artInfo, ArtworkType artworkType) {
        ImageContainer c = new ImageContainer(imageView, paletteObserver);
        RequestKey k = new RequestKey(artInfo, artworkType);
        queueRequest(c, k, true);
        return c;
    }

    @Override
    public Subscription newAlbumRequest(AnimatedImageView imageView, PaletteObserver paletteObserver, long albumId,
            ArtworkType artworkType) {
        return new WrappedImageContainer(imageView, paletteObserver, albumId, artworkType);
    }

    @Override
    public Subscription newArtistRequest(AnimatedImageView imageView, PaletteObserver paletteObserver,
            ArtInfo artInfo, ArtworkType artworkType) {
        ImageContainer c = new ImageContainer(imageView, paletteObserver);
        RequestKey k = new RequestKey(artInfo, artworkType);
        queueRequest(c, k, false);
        return c;
    }

    @Override
    public ParcelFileDescriptor getArtwork(String artistName, String albumName) {
        final ArtInfo artInfo = new ArtInfo(artistName, albumName, null);
        final String cacheKey = getCacheKey(artInfo, ArtworkType.LARGE);
        ParcelFileDescriptor pfd = pullSnapshot(cacheKey);
        // Create request so it will be there next time
        if (pfd == null)
            newAlbumRequest(null, null, artInfo, ArtworkType.LARGE);
        return pfd;
    }

    @Override
    public ParcelFileDescriptor getArtworkThumbnail(String artistName, String albumName) {
        final ArtInfo artInfo = new ArtInfo(artistName, albumName, null);
        final String cacheKey = getCacheKey(artInfo, ArtworkType.THUMBNAIL);
        ParcelFileDescriptor pfd = pullSnapshot(cacheKey);
        // Create request so it will be there next time
        if (pfd == null)
            newAlbumRequest(null, null, artInfo, ArtworkType.THUMBNAIL);
        return pfd;
    }

    @Override
    public boolean clearCaches() {
        boolean success;
        clearVolleyQueue();
        mVolleyQueue.getCache().clear();
        evictL1();
        success = mL2Cache.clearCache();
        return success;
    }

    @Override
    @DebugLog
    public void evictL1() {
        mL1Cache.clearCache();
    }

    @Override
    @DebugLog
    public void onDeathImminent() {
        //        diskCacheQueue.clear();
        clearVolleyQueue();
    }

    /*
     * End IMPL
     */

    void queueRequest(ImageContainer c, RequestKey k, boolean isAlbum) {
        IArtworkRequest r = mActiveRequests.get(k);
        if (r == null) {
            r = isAlbum ? new AlbumArtworkRequest(k) : new ArtistArtworkRequest(k);
            mActiveRequests.put(k, r);
        } else {
            Timber.d("Attaching recipient to running request %s", k.artInfo);
        }
        r.addRecipient(c);
    }

    void clearVolleyQueue() {
        mVolleyQueue.cancelAll(new RequestQueue.RequestFilter() {
            @Override
            public boolean apply(Request<?> request) {
                return true;
            }
        });
    }

    public Observable<CacheResponse> createCacheObservable(final ArtInfo artInfo, final ArtworkType artworkType) {
        final String cacheKey = getCacheKey(artInfo, artworkType);
        return Observable.create(new Observable.OnSubscribe<CacheResponse>() {
            @Override
            public void call(Subscriber<? super CacheResponse> subscriber) {
                //                    Timber.v("Trying L1 for %s, from %s", cacheKey, Thread.currentThread().getName());
                Artwork artwork = mL1Cache.getArtwork(cacheKey);
                if (!subscriber.isUnsubscribed()) {
                    if (artwork != null) {
                        subscriber.onNext(new CacheResponse(artwork, true));
                        subscriber.onCompleted();
                    } else {
                        subscriber.onError(new CacheMissException());
                    }
                }
            }
        })
                // We missed the l1cache try l2
                .onErrorResumeNext(new Func1<Throwable, Observable<? extends CacheResponse>>() {
                    @Override
                    public Observable<? extends CacheResponse> call(Throwable throwable) {
                        if (throwable instanceof CacheMissException) {
                            return Observable.create(new Observable.OnSubscribe<CacheResponse>() {
                                @Override
                                public void call(Subscriber<? super CacheResponse> subscriber) {
                                    //                                Timber.v("Trying L2 for %s, from %s", cacheKey, Thread.currentThread().getName());
                                    Bitmap bitmap = mL2Cache.getBitmap(cacheKey);
                                    if (bitmap != null) {
                                        //Always add to cache
                                        Palette palette = Palette.generate(bitmap);
                                        Artwork artwork = new Artwork(bitmap, palette);
                                        mL1Cache.putArtwork(cacheKey, artwork);
                                        if (subscriber.isUnsubscribed())
                                            return;
                                        subscriber.onNext(new CacheResponse(artwork, false));
                                        subscriber.onCompleted();
                                    } else {
                                        if (subscriber.isUnsubscribed())
                                            return;
                                        subscriber.onError(new CacheMissException());
                                    }
                                }
                            }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
                        } else {
                            return Observable.error(throwable);
                        }
                    }
                });
    }

    public Observable<Artwork> createAlbumNetworkObservable(final ArtInfo artInfo, final ArtworkType artworkType) {
        return createAlbumLastFmApiRequestObservable(artInfo)
                // remap the album info returned by last fm into a url where we can find an image
                .flatMap(new Func1<Album, Observable<String>>() {
                    @Override
                    public Observable<String> call(final Album album) {
                        // try coverartarchive
                        if (!mPreferences.getBoolean(AppPreferences.WANT_LOW_RESOLUTION_ART, false)) {
                            Timber.v("Creating CoverArtRequest %s, from %s", album.getName(),
                                    Thread.currentThread().getName());
                            return createAlbumCoverArtRequestObservable(album.getMbid())
                                    // if coverartarchive fails fallback to lastfm
                                    // im using ResumeNext so i can propogate the error
                                    // not sure Return will do that properly TODO find out
                                    .onErrorResumeNext(new Func1<Throwable, Observable<String>>() {
                                        @Override
                                        public Observable<String> call(Throwable throwable) {
                                            Timber.v("CoverArtRequest failed %s, from %s", album.getName(),
                                                    Thread.currentThread().getName());
                                            String url = getBestImage(album, true);
                                            if (!TextUtils.isEmpty(url)) {
                                                return Observable.just(url);
                                            } else {
                                                return Observable.error(new NullPointerException(
                                                        "No image urls for " + album.getName()));
                                            }
                                        }
                                    });
                        } else { // user wants low res go straight for lastfm
                            String url = getBestImage(album, false);
                            if (!TextUtils.isEmpty(url)) {
                                return Observable.just(url);
                            } else {
                                return Observable.error(new NullPointerException("No url for " + album.getName()));
                            }
                        }
                    }
                })
                // remap the url we found into a bitmap
                .flatMap(new Func1<String, Observable<Artwork>>() {
                    @Override
                    public Observable<Artwork> call(String s) {
                        return createImageRequestObservable(s, artInfo, artworkType);
                    }
                });
    }

    public Observable<Artwork> createArtistNetworkRequest(final ArtInfo artInfo, final ArtworkType artworkType) {
        return createArtistLastFmApiRequestObservable(artInfo).map(new Func1<Artist, String>() {
            @Override
            public String call(Artist artist) {
                String url = getBestImage(artist,
                        !mPreferences.getBoolean(AppPreferences.WANT_LOW_RESOLUTION_ART, false));
                if (!TextUtils.isEmpty(url)) {
                    return url;
                }
                Timber.v("ArtistApiRequest: No image urls for %s", artist.getName());
                throw new NullPointerException("No image urls for " + artist.getName());
            }
        }).flatMap(new Func1<String, Observable<Artwork>>() {
            @Override
            public Observable<Artwork> call(String s) {
                return createImageRequestObservable(s, artInfo, artworkType);
            }
        });
    }

    public Observable<Album> createAlbumLastFmApiRequestObservable(final ArtInfo artInfo) {
        return Observable.create(new Observable.OnSubscribe<Album>() {
            @Override
            public void call(final Subscriber<? super Album> subscriber) {
                MusicEntryResponseCallback<Album> listener = new MusicEntryResponseCallback<Album>() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onError(volleyError);
                    }

                    @Override
                    public void onResponse(Album album) {
                        if (subscriber.isUnsubscribed())
                            return;
                        if (!TextUtils.isEmpty(album.getMbid())) {
                            subscriber.onNext(album);
                            subscriber.onCompleted();
                        } else {
                            Timber.w("Api response does not contain mbid for %s", album.getName());
                            onErrorResponse(new VolleyError("Unknown mbid"));
                        }
                    }
                };
                mVolleyQueue.add(
                        Fetch.albumInfo(artInfo.artistName, artInfo.albumName, listener, Request.Priority.HIGH));
            }
        });
    }

    public Observable<Artist> createArtistLastFmApiRequestObservable(final ArtInfo artInfo) {
        return Observable.create(new Observable.OnSubscribe<Artist>() {
            @Override
            public void call(final Subscriber<? super Artist> subscriber) {
                MusicEntryResponseCallback<Artist> listener = new MusicEntryResponseCallback<Artist>() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onError(volleyError);
                    }

                    @Override
                    public void onResponse(Artist artist) {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onNext(artist);
                        subscriber.onCompleted();
                    }
                };
                mVolleyQueue.add(Fetch.artistInfo(artInfo.artistName, listener, Request.Priority.HIGH));
            }
        });
    }

    public Observable<String> createAlbumCoverArtRequestObservable(final String mbid) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(final Subscriber<? super String> subscriber) {
                CoverArtJsonRequest.Listener listener = new CoverArtJsonRequest.Listener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {
                        Timber.v("CoverArtRequest:onErrorResponse %s", volleyError);
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onError(volleyError);
                    }

                    @Override
                    public void onResponse(String s) {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onNext(s);
                        subscriber.onCompleted();
                    }
                };
                mVolleyQueue.add(new CoverArtJsonRequest(mbid, listener, mGson));
            }
        });
    }

    public Observable<Artwork> createImageRequestObservable(final String url, final ArtInfo artInfo,
            final ArtworkType artworkType) {
        return Observable.create(new Observable.OnSubscribe<Artwork>() {
            @Override
            public void call(final Subscriber<? super Artwork> subscriber) {
                Timber.v("creating ImageRequest %s, from %s", url, Thread.currentThread().getName());
                ArtworkRequest2.Listener listener = new ArtworkRequest2.Listener() {
                    @Override
                    public void onErrorResponse(VolleyError volleyError) {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onError(volleyError);
                    }

                    @Override
                    public void onResponse(Artwork artwork) {
                        // always add to cache
                        String cacheKey = getCacheKey(artInfo, artworkType);
                        mL1Cache.putArtwork(cacheKey, artwork);
                        putInDiskCache(cacheKey, artwork.bitmap);
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onNext(artwork);
                        subscriber.onCompleted();
                    }
                };
                mVolleyQueue.add(new ArtworkRequest2(url, artworkType, listener).setTag(artInfo));
                // Here we take advantage of volleys coolest feature,
                // We have 2 types of images, a thumbnail and a larger image suitable for
                // fullscreen use. these are almost never required at the same time so we create
                // a second request. The cool part is volley wont actually download the image twice
                // this second request is attached to the first and processed afterwards
                // saving bandwidth and ensuring both kinds of images are available next time
                // we need them.
                createImageRequestForCache(url, artInfo, ArtworkType.opposite(artworkType));
            }
        });
    }

    public void createImageRequestForCache(final String url, final ArtInfo artInfo, final ArtworkType artworkType) {
        ArtworkRequest2.Listener listener = new ArtworkRequest2.Listener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                //pass
            }

            @Override
            public void onResponse(Artwork artwork) {
                putInDiskCache(getCacheKey(artInfo, artworkType), artwork.bitmap);
            }
        };
        mVolleyQueue.add(new ArtworkRequest2(url, artworkType, listener).setTag(artInfo));
    }

    public Observable<Artwork> createMediaStoreRequestObservable(final ArtInfo artInfo,
            final ArtworkType artworkType) {
        return Observable.create(new Observable.OnSubscribe<Artwork>() {
            @Override
            public void call(final Subscriber<? super Artwork> subscriber) {
                Timber.v("creating MediaStoreRequest %s, from %s", artInfo, Thread.currentThread().getName());
                InputStream in = null;
                try {
                    final Uri uri = artInfo.artworkUri;
                    in = mContext.getContentResolver().openInputStream(uri);
                    final NetworkResponse response = new NetworkResponse(IOUtils.toByteArray(in));
                    // We create a faux ImageRequest so we can cheat and use it
                    // to processs the bitmap like a real network request
                    // this is not only easier but safer since all bitmap processing is serial.
                    ArtworkRequest2 request = new ArtworkRequest2("fauxrequest", artworkType, null);
                    Response<Artwork> result = request.parseNetworkResponse(response);
                    // make the opposite as well
                    ArtworkType artworkType2 = ArtworkType.opposite(artworkType);
                    ArtworkRequest2 request2 = new ArtworkRequest2("fauxrequest2", artworkType2, null);
                    Response<Artwork> result2 = request2.parseNetworkResponse(response);
                    // add to cache first so we dont return before its added
                    if (result2.isSuccess()) {
                        String cacheKey = getCacheKey(artInfo, artworkType2);
                        putInDiskCache(cacheKey, result2.result.bitmap);
                    }
                    if (result.isSuccess()) {
                        //always add to cache
                        String cacheKey = getCacheKey(artInfo, artworkType);
                        mL1Cache.putArtwork(cacheKey, result.result);
                        putInDiskCache(cacheKey, result.result.bitmap);
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onNext(result.result);
                        subscriber.onCompleted();
                    } else {
                        if (subscriber.isUnsubscribed())
                            return;
                        subscriber.onError(result.error);
                    }
                } catch (Exception e) { //too many to keep track of
                    if (subscriber.isUnsubscribed())
                        return;
                    subscriber.onError(e);
                } finally {
                    IOUtils.closeQuietly(in);
                }
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    final BlockingDeque<Map.Entry<String, Bitmap>> diskCacheQueue = new LinkedBlockingDeque<>();
    Scheduler.Worker diskCacheWorker;

    public void putInDiskCache(final String key, final Bitmap bitmap) {
        Timber.v("putInDiskCache(%s)", key);
        diskCacheQueue.addLast(new AbstractMap.SimpleEntry<>(key, bitmap));
        if (diskCacheWorker == null || diskCacheWorker.isUnsubscribed()) {
            diskCacheWorker = Schedulers.io().createWorker();
            diskCacheWorker.schedule(new Action0() {
                @Override
                public void call() {
                    while (true) {
                        try {
                            Map.Entry<String, Bitmap> entry = diskCacheQueue.pollFirst(60, TimeUnit.SECONDS);
                            if (entry != null) {
                                writeToL2(entry.getKey(), entry.getValue());
                                continue;
                            }
                        } catch (InterruptedException ignored) {
                            //fall
                        }
                        if (diskCacheWorker != null) {
                            diskCacheWorker.unsubscribe();
                            diskCacheWorker = null;
                        }
                        break;
                    }
                }
            });
        }
    }

    void writeToL2(final String key, final Bitmap bitmap) {
        Timber.v("writeToL2(%s)", key);
        mL2Cache.putBitmap(key, bitmap);
    }

    private ParcelFileDescriptor pullSnapshot(String cacheKey) {
        Timber.d("Checking DiskCache for " + cacheKey);
        try {
            if (mL2Cache == null) {
                throw new IOException("Unable to obtain cache instance");
            }
            final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
            final OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);
            final DiskLruCache.Snapshot snapshot = mL2Cache.getSnapshot(cacheKey);
            if (snapshot != null && snapshot.getInputStream(0) != null) {
                final Scheduler.Worker worker = Schedulers.io().createWorker();
                worker.schedule(new Action0() {
                    @Override
                    public void call() {
                        try {
                            IOUtils.copy(snapshot.getInputStream(0), out);
                        } catch (IOException e) {
                            //                            e.printStackTrace();
                        } finally {
                            snapshot.close();
                            IOUtils.closeQuietly(out);
                            worker.unsubscribe();
                        }
                    }
                });
                return pipe[0];
            } else {
                pipe[0].close();
                out.close();
            }
        } catch (IOException e) {
            Timber.w(e, "pullSnapshot");
        }
        return null;
    }

    /**
     * Creates a cache key for use with the L1 cache.
     *
     * if both artist and album are null we must use the uri or all
     * requests will return the same cache key, to maintain backwards
     * compat with orpheus versions < 0.5 we use artist,album when we can.
     *
     * if all fields in artinfo are null we cannot fetch any art so an npe will
     * be thrown
     */
    public static String getCacheKey(ArtInfo artInfo, ArtworkType imageType) {
        int size = 0;
        if (artInfo.artistName == null && artInfo.albumName == null) {
            if (artInfo.artworkUri == null) {
                throw new NullPointerException("Cant fetch art with all null fields");
            }
            size += artInfo.artworkUri.toString().length();
            return new StringBuilder(size + 12).append("#").append(imageType).append("#")
                    .append(artInfo.artworkUri.toString()).toString();
        } else {
            size += artInfo.artistName != null ? artInfo.artistName.length() : 4;
            size += artInfo.albumName != null ? artInfo.albumName.length() : 4;
            return new StringBuilder(size + 12).append("#").append(imageType).append("#").append(artInfo.artistName)
                    .append("#").append(artInfo.albumName).toString();
        }
    }

    /**
     * @return url string for highest quality image available or null if none
     */
    public static String getBestImage(MusicEntry e, boolean wantHigResArt) {
        for (ImageSize q : ImageSize.values()) {
            if (q.equals(ImageSize.MEGA) && !wantHigResArt) {
                continue;
            }
            String url = e.getImageURL(q);
            if (!TextUtils.isEmpty(url)) {
                Timber.v("Found " + q.toString() + " url for " + e.getName());
                return url;
            }
        }
        return null;
    }

    /**
     *
     */
    public static boolean isLocalArtwork(Uri u) {
        if (u != null) {
            if ("content".equals(u.getScheme())) {
                return true;
            }
        }
        return false;
    }

    boolean isOnline(boolean wifiOnly) {

        boolean state = false;

        /* Wi-Fi connection */
        final NetworkInfo wifiNetwork = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        if (wifiNetwork != null) {
            state = wifiNetwork.isConnectedOrConnecting();
        }

        // Don't bother checking the rest if we are connected or we have opted out of mobile
        if (wifiOnly || state) {
            return state;
        }

        /* Mobile data connection */
        final NetworkInfo mbobileNetwork = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
        if (mbobileNetwork != null) {
            state = mbobileNetwork.isConnectedOrConnecting();
        }

        /* Other networks */
        final NetworkInfo activeNetwork = mConnectivityManager.getActiveNetworkInfo();
        if (activeNetwork != null) {
            state = activeNetwork.isConnectedOrConnecting();
        }

        return state;
    }

}