ultrasonic-app-subsonic-and.../ultrasonic/src/main/java/org/moire/ultrasonic/util/LegacyImageLoader.java

456 lines
14 KiB
Java

/*
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 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.
* <p/>
* 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<String, Bitmap> cache = new LRUCache<>(150);
private final BlockingQueue<Task> queue;
private int imageSizeDefault;
private final int imageSizeLarge;
private Bitmap largeUnknownImage;
private Bitmap unknownAvatarImage;
private Context context;
private Collection<Thread> 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<Thread>(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);
}
}
}
}