github.madmarty.madsonic.util.ImageLoader.java Source code

Java tutorial

Introduction

Here is the source code for github.madmarty.madsonic.util.ImageLoader.java

Source

/*
 This file is part of Subsonic.
    
 Subsonic 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.
    
 Subsonic 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 Subsonic.  If not, see <http://www.gnu.org/licenses/>.
    
 Copyright 2009 (C) Sindre Mehus
 */
package github.madmarty.madsonic.util;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.media.RemoteControlClient;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.Log;
import android.support.v4.util.LruCache;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import github.madmarty.madsonic.R;
import github.madmarty.madsonic.domain.MusicDirectory;
import github.madmarty.madsonic.domain.MusicDirectory.Entry;
import github.madmarty.madsonic.service.MusicService;
import github.madmarty.madsonic.service.MusicServiceFactory;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Asynchronous loading of images, with caching.
 * <p/>
 * There should normally be only one instance of this class.
 *
 * @author Sindre Mehus
 */
@TargetApi(14)
public class ImageLoader implements Runnable {

    private static final Logger LOG = new Logger(ImageLoader.class);
    private static final int CONCURRENCY = 6;

    private Handler mHandler = new Handler();
    private Context context;

    private LruCache<String, Bitmap> cache;
    private final BlockingQueue<Task> queue;

    private final int imageSizeDefault;
    private final int imageSizeMedium;
    private final int imageSizeLarge;

    private Bitmap unknownAvatarImage;

    //    private final int imageSizeSmall  = 128;
    //    private final int imageSizeMedium = 192;
    //    private final int imageSizeLarge  = 256;
    //    private final int imageSizeXLarge = 512;

    private Drawable largeUnknownImage;

    public ImageLoader(Context context) {

        this.context = context;

        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 4;

        cache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) {
                if (evicted) {
                    try {
                        oldBitmap.recycle();
                    } catch (Exception e) {
                        // Do nothing, just means that the drawable is a flat image
                    }
                }
            }
        };

        queue = new LinkedBlockingQueue<Task>(1000);

        // Determine the density-dependent image sizes.
        imageSizeDefault = (int) Math
                .round((context.getResources().getDrawable(R.drawable.unknown_album).getIntrinsicHeight()));
        LOG.info("imageSizeDefault: " + imageSizeDefault);

        imageSizeMedium = 180; // (int) Math.round((context.getResources().getDrawable(R.drawable.unknown_album_medium).getIntrinsicHeight()));;
        LOG.info("imageSizeMedium: " + imageSizeMedium);

        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        imageSizeLarge = (int) Math.round(Math.min(metrics.widthPixels, metrics.heightPixels) * 0.6);
        LOG.info("imageSizeLarge: " + imageSizeLarge);

        //        imageSizeDefault = Util.getCoverSize(context);

        for (int i = 0; i < CONCURRENCY; i++) {
            new Thread(this, "ImageLoader").start();
        }

        createLargeUnknownImage(context);
    }

    private void createLargeUnknownImage(Context context) {

        BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album);
        Bitmap bitmap = Bitmap.createScaledBitmap(drawable.getBitmap(), imageSizeLarge, imageSizeLarge, true);
        bitmap = createReflection(bitmap);
        largeUnknownImage = Util.createDrawableFromBitmap(context, bitmap);
    }

    public void loadImage(View view) {
        setDefaultImage(view);
    }

    // wrapper for reflection
    public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade) {
        loadImage(view, entry, large, crossfade, large);
    }

    public void loadImage(View view, MusicDirectory.Entry entry, boolean large, boolean crossfade,
            boolean reflection) {
        if (entry == null || entry.getCoverArt() == null) {
            setUnknownImage(view, large);
            return;
        }

        int size = large ? imageSizeLarge : imageSizeDefault;

        //        Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size));
        //        if (bitmap != null) {
        //         final Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
        //            setImage(view, drawable, large);
        //            return;

        Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), size));
        if (bitmap != null) {
            // Create a clone since the images can be modified by the caller.
            Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
            Drawable clone = drawable.getConstantState().newDrawable();
            setImage(view, clone, large);
            return;
        }

        if (!large) {
            setUnknownImage(view, large);
        }
        queue.offer(new Task(view.getContext(), entry, size, imageSizeLarge, large ? true : false,
                new ViewTaskHandler(view, crossfade)));
    }

    public void loadImage(Context context, RemoteControlClient remoteControl, MusicDirectory.Entry entry) {
        if (entry == null || entry.getCoverArt() == null) {
            setUnknownImage(remoteControl);
            return;
        }

        Bitmap bitmap = cache.get(getKey(entry.getCoverArt(), imageSizeLarge));
        if (bitmap != null && !bitmap.isRecycled()) {
            Drawable drawable = Util.createDrawableFromBitmap(this.context, bitmap);
            setImage(remoteControl, drawable);
            return;
        }

        setUnknownImage(remoteControl);
        queue.offer(new Task(context, entry, imageSizeLarge, imageSizeLarge, false,
                new RemoteControlClientTaskHandler(remoteControl)));
    }

    private String getKey(String coverArtId, int size) {
        return coverArtId + size;
    }

    @SuppressWarnings("deprecation")
    private void setImage(View view, Drawable drawable, boolean crossfade) {
        if (view instanceof TextView) {
            // Cross-fading is not implemented for TextView since it's not in use.  It would be easy to add it, though.
            TextView textView = (TextView) view;
            textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
        } else if (view instanceof ImageView) {
            ImageView imageView = (ImageView) view;
            if (crossfade) {

                Drawable existingDrawable = imageView.getDrawable();
                if (existingDrawable == null) {
                    Bitmap emptyImage;
                    if (drawable.getIntrinsicWidth() > 0 && drawable.getIntrinsicHeight() > 0) {
                        emptyImage = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
                    } else {
                        emptyImage = Bitmap.createBitmap(imageSizeDefault, imageSizeDefault,
                                Bitmap.Config.ARGB_8888);
                    }
                    existingDrawable = new BitmapDrawable(emptyImage);
                } else {
                    // Try to get rid of old transitions
                    try {
                        TransitionDrawable tmp = (TransitionDrawable) existingDrawable;
                        int layers = tmp.getNumberOfLayers();
                        existingDrawable = tmp.getDrawable(layers - 1);
                    } catch (Exception e) {
                        // Do nothing, just means that the drawable is a flat image
                    }
                }

                Drawable[] layers = new Drawable[] { existingDrawable, drawable };

                TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
                imageView.setImageDrawable(transitionDrawable);
                transitionDrawable.startTransition(250);
            } else {
                imageView.setImageDrawable(drawable);
            }
        }
    }

    private void setImage(RemoteControlClient remoteControl, Drawable drawable) {
        if (remoteControl != null && drawable != null) {
            Bitmap origBitmap = ((BitmapDrawable) drawable).getBitmap();
            remoteControl.editMetadata(false).putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK,
                    origBitmap.copy(origBitmap.getConfig(), true)).apply();
        }
    }

    private void setDefaultImage(View view) {
        if (view instanceof TextView) {
            ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_default, 0, 0, 0);
        } else if (view instanceof ImageView) {
            ((ImageView) view).setImageResource(R.drawable.unknown_default);
        }
    }

    private void setUnknownImage(View view, boolean large) {
        if (large) {
            setImage(view, largeUnknownImage, true);
        } else {
            if (view instanceof TextView) {
                ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(R.drawable.unknown_album, 0, 0, 0);
            } else if (view instanceof ImageView) {
                ((ImageView) view).setImageResource(R.drawable.unknown_album);
            }
        }
    }

    private void setUnknownImage(RemoteControlClient remoteControl) {
        setImage(remoteControl, largeUnknownImage);
    }

    public void clear() {
        queue.clear();
    }

    @Override
    public void run() {
        while (true) {
            try {
                Task task = queue.take();
                task.execute();
            } catch (Throwable x) {
                LOG.error("Unexpected exception in ImageLoader.", x);
            }
        }
    }

    private Bitmap createReflection(Bitmap originalImage) {

        //   int reflectionH = 80;

        int width = originalImage.getWidth();
        int height = originalImage.getHeight();

        // Height of reflection
        int reflectionHeight = height / 2;

        // The gap we want between the reflection and the original image
        final int reflectionGap = 4;

        // Create a new bitmap with same width but taller to fit reflection
        Bitmap bitmapWithReflection = Bitmap.createBitmap(width, (height + reflectionHeight),
                Bitmap.Config.ARGB_8888);

        //// ----

        Bitmap reflection = Bitmap.createBitmap(width, reflectionHeight, Bitmap.Config.ARGB_8888);
        Bitmap blurryBitmap = Bitmap.createBitmap(originalImage, 0, height - reflectionHeight, height,
                reflectionHeight);

        // cheap and easy scaling algorithm; down-scale it, then
        // upscale it. The filtering during the scale operations
        // will blur the resulting image
        blurryBitmap = Bitmap
                .createScaledBitmap(
                        Bitmap.createScaledBitmap(blurryBitmap, blurryBitmap.getWidth() / 2,
                                blurryBitmap.getHeight() / 2, true),
                        blurryBitmap.getWidth(), blurryBitmap.getHeight(), true);

        // This shadier will hold a cropped, inverted,
        // blurry version of the original image
        BitmapShader bitmapShader = new BitmapShader(blurryBitmap, TileMode.CLAMP, TileMode.CLAMP);
        Matrix invertMatrix = new Matrix();
        invertMatrix.setScale(1f, -1f);
        invertMatrix.preTranslate(0, -reflectionHeight);
        bitmapShader.setLocalMatrix(invertMatrix);

        // This shader holds an alpha gradient
        Shader alphaGradient = new LinearGradient(0, 0, 0, reflectionHeight, 0x80ffffff, 0x00000000,
                TileMode.CLAMP);

        // This shader combines the previous two, resulting in a
        // blurred, fading reflection
        ComposeShader compositor = new ComposeShader(bitmapShader, alphaGradient, PorterDuff.Mode.DST_IN);

        Paint reflectionPaint = new Paint();
        reflectionPaint.setShader(compositor);

        // Draw the reflection into the bitmap that we will return
        Canvas canvas = new Canvas(reflection);
        canvas.drawRect(0, 0, reflection.getWidth(), reflection.getHeight(), reflectionPaint);

        /// -----

        // Create a new Canvas with the bitmap that's big enough for
        // the image plus gap plus reflection
        Canvas finalcanvas = new Canvas(bitmapWithReflection);

        // Draw in the original image
        finalcanvas.drawBitmap(originalImage, 0, 0, null);

        // Draw in the gap
        Paint defaultPaint = new Paint();

        // transparent gap
        defaultPaint.setColor(0);

        finalcanvas.drawRect(0, height, width, height + reflectionGap, defaultPaint);

        // Draw in the reflection
        finalcanvas.drawBitmap(reflection, 0, height + reflectionGap, null);

        return bitmapWithReflection;
    }

    private class Task {

        private final Context mContext;
        private final View mView;
        private final MusicDirectory.Entry mEntry;
        private final String username;
        private final int mSize;
        private final int mSaveSize;
        private final boolean mReflection;
        private final Handler handler;
        private ImageLoaderTaskHandler mTaskHandler;

        public Task(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean reflection,
                ImageLoaderTaskHandler taskHandler) {
            mContext = context;
            mView = null;
            mEntry = entry;
            mSize = size;
            username = null;
            mReflection = reflection;
            mSaveSize = saveSize;
            mTaskHandler = taskHandler;
            handler = null;
        }

        public Task(View view, String username, int size, int saveSize, boolean reflection) {
            this.mContext = null;
            this.mView = view;
            this.mEntry = null;
            this.username = username;
            this.mSize = size;
            this.mSaveSize = saveSize;
            this.mReflection = reflection;
            this.handler = new Handler();
        }

        public void execute() {
            try {
                MusicService musicService;

                if (mView == null) {
                    musicService = MusicServiceFactory.getMusicService(mContext);
                } else {
                    musicService = MusicServiceFactory.getMusicService(mView.getContext());

                }

                boolean isAvatar = this.username != null && this.mEntry == null;

                Bitmap bitmap = null;

                if (this.mEntry == null) {
                    bitmap = musicService.getAvatar(mView.getContext(), username, mSize, mSaveSize, null);
                    if (bitmap == null) {
                        return;
                    }
                }

                if (isAvatar) {
                    addImageToCache(bitmap, username, mSize);
                    setAvatarImageBitmap(mView, username, bitmap, false);
                } else {
                    try {
                        loadImage();

                    } catch (OutOfMemoryError e) {
                        LOG.warn("Ran out of memory trying to load image, try cleanup and retry");
                        cache.evictAll();
                        System.gc();
                    }
                }
            } catch (Throwable x) {
                LOG.error("Failed to download album art.", x);
            }

        }

        private void setImageBitmap(View view, MusicDirectory.Entry entry, Bitmap bitmap, boolean crossFade) {
            if (view instanceof ImageView) {
                ImageView imageView = (ImageView) view;

                MusicDirectory.Entry tagEntry = (MusicDirectory.Entry) view.getTag();

                // Only apply image to the view if the view is intended for this entry
                if (entry != null && tagEntry != null && !entry.equals(tagEntry)) {
                    LOG.info("View is no longer valid, not setting ImageBitmap");
                    return;
                }

                if (crossFade) {
                    Drawable existingDrawable = imageView.getDrawable();
                    Drawable newDrawable = Util.createDrawableFromBitmap(context, bitmap);

                    if (existingDrawable == null) {
                        Bitmap emptyImage = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
                                Bitmap.Config.ARGB_8888);
                        existingDrawable = new BitmapDrawable(context.getResources(), emptyImage);
                    }

                    Drawable[] layers = new Drawable[] { existingDrawable, newDrawable };

                    TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
                    imageView.setImageDrawable(transitionDrawable);
                    transitionDrawable.startTransition(250);
                } else {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }

        public void loadImage() {
            try {
                MusicService musicService = MusicServiceFactory.getMusicService(mContext);
                Bitmap bitmap = musicService.getCoverArt(mContext, mEntry, mSize, mSaveSize, null);
                String key = getKey(mEntry.getCoverArt(), mSize);

                if (mReflection) {
                    bitmap = createReflection(bitmap);
                }

                if (mSize != imageSizeLarge) {
                    cache.put(key, bitmap);
                    // Make sure key is the most recently "used"
                    cache.get(key);
                }

                final Drawable drawable = Util.createDrawableFromBitmap(mContext, bitmap);
                mTaskHandler.setDrawable(drawable);
                mHandler.post(mTaskHandler);
            } catch (Throwable x) {
                LOG.error("Failed to download album art.", x);
            }
        }
    }

    private void setAvatarImageBitmap(View view, String username, Bitmap bitmap, boolean crossFade) {
        if (view instanceof ImageView) {
            ImageView imageView = (ImageView) view;

            String tagEntry = (String) view.getTag();

            // Only apply image to the view if the view is intended for this entry
            if (username != null && tagEntry != null && !username.equals(tagEntry)) {
                LOG.info("View is no longer valid, not setting ImageBitmap");
                return;
            }

            if (bitmap != null) {
                try {
                    imageView.setImageBitmap(bitmap);
                } catch (Throwable x) {
                }
            }
        }
    }

    public void setUnknownAvatarImage(View view) {
        setAvatarImageBitmap(view, null, unknownAvatarImage, false);
    }

    private abstract class ImageLoaderTaskHandler implements Runnable {

        protected Drawable mDrawable;

        public void setDrawable(Drawable drawable) {
            mDrawable = drawable;
        }

    }

    private class ViewTaskHandler extends ImageLoaderTaskHandler {

        protected boolean mCrossfade;
        private View mView;

        public ViewTaskHandler(View view, boolean crossfade) {
            mCrossfade = crossfade;
            mView = view;
        }

        @Override
        public void run() {
            setImage(mView, mDrawable, mCrossfade);
        }
    }

    private class RemoteControlClientTaskHandler extends ImageLoaderTaskHandler {

        private RemoteControlClient mRemoteControl;

        public RemoteControlClientTaskHandler(RemoteControlClient remoteControl) {
            mRemoteControl = remoteControl;
        }

        @Override
        public void run() {
            setImage(mRemoteControl, mDrawable);
        }
    }

    private void createUnknownAvatarImage(Context context) {
        Resources res = context.getResources();
        Drawable contact = res.getDrawable(R.drawable.ic_contact_picture);
        unknownAvatarImage = Util.createBitmapFromDrawable(contact);
    }

    public void loadAvatarImage(ImageView view, String username, boolean large, int size, boolean crossFade) {
        view.invalidate();

        if (username == null) {
            //         createUnknownAvatarImage(context);
            setUnknownAvatarImage(view);
            return;
        }

        if (size <= 0) {
            size = large ? imageSizeLarge : imageSizeDefault;
        }

        Bitmap bitmap = cache.get(getKey(username, size));

        if (bitmap != null) {
            setAvatarImageBitmap(view, username, bitmap, crossFade);
            return;
        }

        setUnknownAvatarImage(view);

        queue.offer(new Task(view, username, size, imageSizeDefault, crossFade));
    }

    public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size) {
        if (entry == null) {
            return null;
        }

        String coverArt = entry.getCoverArt();

        if (coverArt == null) {
            return null;
        }

        if (size <= 0) {
            size = large ? imageSizeLarge : imageSizeDefault;
        }

        Bitmap bitmap = cache.get(getKey(coverArt, size));

        if (bitmap != null && !bitmap.isRecycled()) {
            Bitmap.Config config = bitmap.getConfig();
            return bitmap.copy(config, false);
        }

        return null;
    }

    public Bitmap getImageBitmap(String username, int size) {
        Bitmap bitmap = cache.get(getKey(username, size));

        if (bitmap != null && !bitmap.isRecycled()) {
            Bitmap.Config config = bitmap.getConfig();
            return bitmap.copy(config, false);
        }

        return null;
    }

    public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size) {
        cache.put(getKey(entry.getCoverArt(), size), bitmap);
    }

    public void addImageToCache(Bitmap bitmap, String username, int size) {
        cache.put(getKey(username, size), bitmap);
    }

}