/*
* Copyright (C) 2014 Fastboot Mobile, LLC.
*
* This program 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.
*
* This program 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 this program;
* if not, see <http://www.gnu.org/licenses>.
*/
package com.fastbootmobile.encore.app.ui;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import com.fastbootmobile.encore.app.R;
import com.fastbootmobile.encore.art.AlbumArtTask;
import com.fastbootmobile.encore.utils.Utils;
import com.fastbootmobile.encore.art.AlbumArtCache;
import com.fastbootmobile.encore.art.AlbumArtHelper;
import com.fastbootmobile.encore.art.RecyclingBitmapDrawable;
import com.fastbootmobile.encore.model.Album;
import com.fastbootmobile.encore.model.Artist;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.ProviderAggregator;
/**
* Square ImageView displaying album art automatically
*/
public class AlbumArtImageView extends SquareImageView implements AlbumArtHelper.AlbumArtListener {
private final String TAG = "AlbumArtImageView";
private static final boolean DEBUG = false;
private static final boolean STUB = false;
private Handler mHandler;
private OnArtLoadedListener mOnArtLoadedListener;
private AlbumArtTask mTask;
private BoundEntity mRequestedEntity;
private MaterialTransitionDrawable mDrawable;
private boolean mCrossfade;
private boolean mSkipTransition;
private RecyclingBitmapDrawable mCurrentBitmap;
private TaskRunnable mRunnable;
private boolean mCurrentIsDefault;
public AlbumArtImageView(Context context) {
super(context);
initialize();
}
public AlbumArtImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public AlbumArtImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize();
}
private void initialize() {
// Set the placeholder art first-hand
mHandler = new Handler();
setScaleType(ScaleType.CENTER_CROP);
if (isInEditMode() || STUB) {
setImageDrawable(getResources().getDrawable(R.drawable.album_placeholder));
} else {
mDrawable = new MaterialTransitionDrawable(
(BitmapDrawable) (getResources().getDrawable(R.drawable.ic_cloud_offline)),
getDefaultBitmap());
setImageDrawable(mDrawable);
}
}
private BitmapDrawable getDefaultBitmap() {
return (BitmapDrawable) getResources().getDrawable(R.drawable.album_placeholder);
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
private void freeMemory(boolean removeRequestedEntity) {
if (mCurrentBitmap != null) {
mCurrentBitmap = null;
if (removeRequestedEntity) {
mRequestedEntity = null;
}
mDrawable.setImmediateTo(getDefaultBitmap());
}
if (mTask != null && !mTask.isCancelled()) {
mTask.cancel(true);
mTask = null;
}
}
@Override
protected void finalize() throws Throwable {
freeMemory(true);
super.finalize();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (DEBUG) Log.d(TAG, "onDetachedFromWindow: mRequestedEntity=" + mRequestedEntity);
freeMemory(false);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (DEBUG) Log.d(TAG, "onAttachedToWindow: mRequestedEntity=" + mRequestedEntity);
if (mRequestedEntity != null && mTask == null && mRunnable == null) {
BoundEntity ent = mRequestedEntity;
mRequestedEntity = null;
loadArtImpl(ent);
}
}
private void forceDrawableReload() {
// Our drawable is likely changing bounds. From ImageView source code, it seems
// like there's no way for the drawable to tell the view "hey, I resized", so we
// manually trigger "updateDrawable()" (a private method in ImageView) by setting
// the drawable again when we know it changes
setImageDrawable(null);
setImageDrawable(mDrawable);
}
/**
* Displays the placeholder album art without transition
*/
public void setDefaultArt() {
if (DEBUG) Log.d(TAG, "setDefaultArt: mCurrentBitmap=" + mCurrentBitmap);
if (mCurrentBitmap != null) {
mCurrentBitmap = null;
}
if (mTask != null) {
mTask.cancel(true);
mTask = null;
}
mDrawable.setImmediateTo(getDefaultBitmap());
forceDrawableReload();
mCurrentIsDefault = true;
}
/**
* Sets the listener that will be called when the art is loaded
*
* @param listener The listener that will be called
*/
public void setOnArtLoadedListener(OnArtLoadedListener listener) {
mOnArtLoadedListener = listener;
}
/**
* When the AlbumArtImageView is in crossfade mode, the album art won't go through the default
* placeholder state before moving into the next album art, but will crossfade to the next
* art directly
*
* @param crossfade true to enable crossfade mode
*/
public void setCrossfade(boolean crossfade) {
mCrossfade = crossfade;
}
public void loadArtForSong(final Song song) {
loadArtImpl(song);
}
public void loadArtForAlbum(final Album album) {
loadArtImpl(album);
if (!STUB) {
// If it's an album and we're offline and album is unavailable,
// overlay the offline status thing
if (ProviderAggregator.getDefault().isOfflineMode()
&& !Utils.isAlbumAvailableOffline(album)) {
mDrawable.setShowOfflineOverdraw(true);
} else {
mDrawable.setShowOfflineOverdraw(false);
}
}
}
public void loadArtForArtist(final Artist artist) {
loadArtImpl(artist);
}
public void loadArtForPlaylist(final Playlist playlist) {
loadArtImpl(playlist);
}
private void loadArtImpl(final BoundEntity ent) {
if (STUB || ent == null ||
(mRequestedEntity != null
&& !mCurrentIsDefault
&& ent.getRef().equals(mRequestedEntity.getRef()))) {
// Nothing to do, we are displaying the proper thing already
return;
}
mSkipTransition = false;
mRequestedEntity = ent;
if (mTask != null) {
mTask.cancel(true);
}
if (mRunnable != null) {
mRunnable.cancel();
mRunnable = null;
}
// If we have the image in cache, show it immediately.
int cacheStatus = AlbumArtCache.getDefault().getCacheStatus(ent);
// We delay the loading slightly to make sure we don't uselessly load an image that is
// being quickly flinged through (the requested entity will change in-between as the
// view is recycled). We don't wait however in crossfade mode as we expect to see the
// image as soon as possible.
mRunnable = new TaskRunnable(ent);
// When we're crossfading, we're assuming we want the image directly
if (!mCrossfade) {
setDefaultArt();
}
if (mCrossfade || cacheStatus == AlbumArtCache.CACHE_STATUS_MEMORY
|| cacheStatus == AlbumArtCache.CACHE_STATUS_DISK) {
if (cacheStatus != AlbumArtCache.CACHE_STATUS_UNAVAILABLE) {
mSkipTransition = true;
}
if (Math.max(getMeasuredHeight(), getMeasuredWidth()) > 0) {
mRunnable.run(true);
} else {
mRunnable.setImmediate(true);
mHandler.post(mRunnable);
}
} else {
mHandler.post(mRunnable);
}
}
@Override
public void onArtLoaded(RecyclingBitmapDrawable output, BoundEntity request) {
if (DEBUG)
Log.d(TAG, "onArtLoaded: mCurrentBitmap=" + mCurrentBitmap + " ; output=" + output);
if (request != mRequestedEntity) {
if (DEBUG) Log.d(TAG, "onArtLoaded: Too late for " + request.getRef());
mRunnable = null;
return;
}
// If we have an actual result, display it!
if (output != null) {
mCurrentBitmap = output;
if (mSkipTransition) {
mDrawable.setTransitionDuration(MaterialTransitionDrawable.SHORT_DURATION);
} else {
mDrawable.setTransitionDuration(MaterialTransitionDrawable.DEFAULT_DURATION);
}
mDrawable.transitionTo(mCurrentBitmap);
forceDrawableReload();
if (mOnArtLoadedListener != null) {
mOnArtLoadedListener.onArtLoaded(this, mCurrentBitmap);
}
mCurrentIsDefault = false;
} else {
mCurrentIsDefault = true;
}
// If it's an album and we're offline and album is unavailable,
// overlay the offline status thing
if (request instanceof Album &&
ProviderAggregator.getDefault().isOfflineMode()
&& !Utils.isAlbumAvailableOffline((Album) request)) {
mDrawable.setShowOfflineOverdraw(true);
}
mRunnable = null;
mTask = null;
}
public interface OnArtLoadedListener {
void onArtLoaded(AlbumArtImageView view, BitmapDrawable drawable);
}
private class TaskRunnable implements Runnable {
private BoundEntity mEntity;
private boolean mImmediate;
public TaskRunnable(BoundEntity ent) {
mEntity = ent;
}
public void setImmediate(boolean immediate) {
mImmediate = immediate;
}
public void run(boolean immediate) {
mImmediate = immediate;
run();
}
@Override
public void run() {
if (mRequestedEntity != null && mEntity != null && mRequestedEntity.equals(mEntity)) {
int size = Math.max(getMeasuredHeight(), getMeasuredWidth());
mTask = AlbumArtHelper.retrieveAlbumArt(getContext().getApplicationContext().getResources(),
AlbumArtImageView.this, mEntity, size, mImmediate);
}
}
public void cancel() {
mEntity = null;
}
}
}