/* 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 . Copyright 2009 (C) Sindre Mehus */ package org.moire.ultrasonic.util; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import org.moire.ultrasonic.R; import org.moire.ultrasonic.domain.MusicDirectory; import org.moire.ultrasonic.service.MusicService; import org.moire.ultrasonic.service.MusicServiceFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; /** * Asynchronous loading of images, with caching. *

* There should normally be only one instance of this class. * * @author Sindre Mehus */ public class LegacyImageLoader implements Runnable, ImageLoader { private static final String TAG = LegacyImageLoader.class.getSimpleName(); private final LRUCache cache = new LRUCache<>(150); private final BlockingQueue queue; private int imageSizeDefault; private final int imageSizeLarge; private Bitmap largeUnknownImage; private Bitmap unknownAvatarImage; private Context context; private Collection threads; private AtomicBoolean running = new AtomicBoolean(); private int concurrency; public LegacyImageLoader( Context context, int concurrency ) { this.context = context; this.concurrency = concurrency; queue = new LinkedBlockingQueue<>(1000); Resources resources = context.getResources(); Drawable drawable = resources.getDrawable(R.drawable.unknown_album); // Determine the density-dependent image sizes. if (drawable != null) { imageSizeDefault = drawable.getIntrinsicHeight(); } imageSizeLarge = Util.getMaxDisplayMetric(context); createLargeUnknownImage(context); createUnknownAvatarImage(context); } @Override public synchronized boolean isRunning() { return running.get() && !threads.isEmpty(); } @Override public void setConcurrency(int concurrency) { this.concurrency = concurrency; } @Override public void startImageLoader() { running.set(true); threads = Collections.synchronizedCollection(new ArrayList(this.concurrency)); for (int i = 0; i < this.concurrency; i++) { Thread thread = new Thread(this, String.format("ImageLoader_%d", i)); threads.add(thread); thread.start(); } } @Override public synchronized void stopImageLoader() { clear(); for (Thread thread : threads) { thread.interrupt(); } running.set(false); threads.clear(); } private void createLargeUnknownImage(Context context) { BitmapDrawable drawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.unknown_album); Log.i(TAG, "createLargeUnknownImage"); if (drawable != null) { largeUnknownImage = Util.scaleBitmap(drawable.getBitmap(), imageSizeLarge); } } private void createUnknownAvatarImage(Context context) { Resources res = context.getResources(); Drawable contact = res.getDrawable(R.drawable.ic_contact_picture); unknownAvatarImage = Util.createBitmapFromDrawable(contact); } @Override public void loadAvatarImage( View view, String username, boolean large, int size, boolean crossFade, boolean highQuality ) { view.invalidate(); if (username == null) { 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, large, crossFade, highQuality)); } @Override public void loadImage( View view, MusicDirectory.Entry entry, boolean large, int size, boolean crossFade, boolean highQuality ) { view.invalidate(); if (entry == null) { setUnknownImage(view, large); return; } String coverArt = entry.getCoverArt(); if (TextUtils.isEmpty(coverArt)) { setUnknownImage(view, large); return; } if (size <= 0) { size = large ? imageSizeLarge : imageSizeDefault; } Bitmap bitmap = cache.get(getKey(coverArt, size)); if (bitmap != null) { setImageBitmap(view, entry, bitmap, crossFade); return; } setUnknownImage(view, large); queue.offer(new Task(view, entry, size, large, crossFade, highQuality)); } private static String getKey(String coverArtId, int size) { return String.format("%s:%d", coverArtId, size); } @Override 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; } @Override public Bitmap getImageBitmap(MusicDirectory.Entry entry, boolean large, int size) { if (entry == null) { return null; } String coverArt = entry.getCoverArt(); if (TextUtils.isEmpty(coverArt)) { 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; } 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.i(TAG, "View is no longer valid, not setting ImageBitmap"); return; } if (crossFade) { Drawable existingDrawable = imageView.getDrawable(); Drawable newDrawable = Util.createDrawableFromBitmap(this.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); } } } 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.i(TAG, "View is no longer valid, not setting ImageBitmap"); return; } if (crossFade) { Drawable existingDrawable = imageView.getDrawable(); Drawable newDrawable = Util.createDrawableFromBitmap(this.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); } } } private void setUnknownAvatarImage(View view) { setAvatarImageBitmap(view, null, unknownAvatarImage, false); } private void setUnknownImage(View view, boolean large) { if (large) { setImageBitmap(view, null, largeUnknownImage, false); } 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); } } } @Override public void addImageToCache(Bitmap bitmap, MusicDirectory.Entry entry, int size) { cache.put(getKey(entry.getCoverArt(), size), bitmap); } @Override public void addImageToCache(Bitmap bitmap, String username, int size) { cache.put(getKey(username, size), bitmap); } @Override public void clear() { queue.clear(); } @Override public void run() { while (running.get()) { try { Task task = queue.take(); task.execute(); } catch (InterruptedException ignored) { running.set(false); break; } catch (Throwable x) { Log.e(TAG, "Unexpected exception in ImageLoader.", x); } } } private class Task { private final View view; private final MusicDirectory.Entry entry; private final String username; private final Handler handler; private final int size; private final boolean saveToFile; private final boolean crossFade; private final boolean highQuality; Task( View view, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean crossFade, boolean highQuality ) { this.view = view; this.entry = entry; this.username = null; this.size = size; this.saveToFile = saveToFile; this.crossFade = crossFade; this.highQuality = highQuality; handler = new Handler(); } Task( View view, String username, int size, boolean saveToFile, boolean crossFade, boolean highQuality ) { this.view = view; this.entry = null; this.username = username; this.size = size; this.saveToFile = saveToFile; this.crossFade = crossFade; this.highQuality = highQuality; handler = new Handler(); } public void execute() { try { MusicService musicService = MusicServiceFactory.getMusicService(view.getContext()); final boolean isAvatar = this.username != null && this.entry == null; final Bitmap bitmap = this.entry != null ? musicService.getCoverArt(view.getContext(), entry, size, saveToFile, highQuality, null) : musicService.getAvatar(view.getContext(), username, size, saveToFile, highQuality, null); if (bitmap == null) { Log.d(TAG, "Found empty album art."); return; } if (isAvatar) addImageToCache(bitmap, username, size); else addImageToCache(bitmap, entry, size); handler.post(new Runnable() { @Override public void run() { if (isAvatar) { setAvatarImageBitmap(view, username, bitmap, crossFade); } else { setImageBitmap(view, entry, bitmap, crossFade); } } }); } catch (Throwable x) { Log.e(TAG, "Failed to download album art.", x); } } } }