Java tutorial
/* 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); } }