/* 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.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.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 = 5; 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 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>(500); // 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); } // WRAP 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) { 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 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 MusicDirectory.Entry mEntry; private final int mSize; private final int mSaveSize; private final boolean mReflection; private ImageLoaderTaskHandler mTaskHandler; public Task(Context context, MusicDirectory.Entry entry, int size, int saveSize, boolean reflection, ImageLoaderTaskHandler taskHandler) { mContext = context; mEntry = entry; mSize = size; mReflection = reflection; mSaveSize = saveSize; mTaskHandler = taskHandler; } public void execute() { try { loadImage(); } catch(OutOfMemoryError e) { LOG.warn( "Ran out of memory trying to load image, try cleanup and retry"); cache.evictAll(); System.gc(); } } 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 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); } } }