/*
* 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.art;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import com.fastbootmobile.encore.app.BuildConfig;
import com.fastbootmobile.encore.model.BoundEntity;
import com.fastbootmobile.encore.model.Playlist;
import com.fastbootmobile.encore.model.Song;
import com.fastbootmobile.encore.providers.IArtCallback;
import com.fastbootmobile.encore.providers.ProviderAggregator;
import java.util.ArrayList;
import java.util.List;
/**
* Class creating a composite image for playlist cover art
*/
public class PlaylistArtBuilder {
private static final String TAG = "PlaylistArtBuilder";
private static final boolean DEBUG = BuildConfig.DEBUG;
private Bitmap mPlaylistComposite;
private final List<RecyclingBitmapDrawable> mPlaylistSource = new ArrayList<>();
private Paint mPlaylistPaint;
private List<AlbumArtTask> mCompositeTasks;
private List<BoundEntity> mCompositeRequests;
private int mNumComposite;
private Handler mHandler;
private Handler mMainHandler;
private HandlerThread mHandlerThread;
private IArtCallback mCallback;
private boolean mDone;
private Runnable mTimeoutWatchdog = new Runnable() {
@Override
public void run() {
if (DEBUG) Log.w(TAG, "Watchdog kicking " + mPlaylistSource.size() + " images");
if (mPlaylistComposite != null && mPlaylistSource.size() > 0) {
try {
mCallback.onArtLoaded(mPlaylistComposite.copy(Bitmap.Config.ARGB_8888, false));
} catch (RemoteException ignored) {
}
}
}
};
private Runnable mUpdatePlaylistCompositeRunnable = new Runnable() {
@Override
public void run() {
if (mPlaylistComposite == null || !mPlaylistComposite.isRecycled() && !mDone) {
makePlaylistComposite();
}
}
};
private AlbumArtHelper.AlbumArtListener mCompositeListener = new AlbumArtHelper.AlbumArtListener() {
@Override
public void onArtLoaded(RecyclingBitmapDrawable output, BoundEntity request) {
if (!mCompositeRequests.contains(request) || mDone) {
return;
}
if (output != null) {
synchronized (mPlaylistSource) {
mPlaylistSource.add(output);
if (mPlaylistSource.size() < mNumComposite) {
mHandler.removeCallbacks(mUpdatePlaylistCompositeRunnable);
mHandler.postDelayed(mUpdatePlaylistCompositeRunnable, 200);
} else {
mHandler.removeCallbacks(mUpdatePlaylistCompositeRunnable);
mHandler.post(mUpdatePlaylistCompositeRunnable);
}
}
}
}
};
public PlaylistArtBuilder() {
mMainHandler = new Handler(Looper.getMainLooper());
mHandlerThread = new HandlerThread("PlaylistArtBuilder");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
public void freeMemory() {
if (mPlaylistComposite != null) {
mPlaylistComposite.recycle();
mPlaylistComposite = null;
}
synchronized (mPlaylistSource) {
mPlaylistSource.clear();
}
if (mCompositeTasks != null) {
for (AlbumArtTask task : mCompositeTasks) {
task.cancel(true);
}
mCompositeTasks.clear();
mCompositeTasks = null;
}
mHandlerThread.interrupt();
}
private void makePlaylistComposite() {
if (mPlaylistComposite == null) {
mPlaylistComposite = Bitmap.createBitmap(600, 600, Bitmap.Config.ARGB_8888);
}
if (mPlaylistPaint == null) {
mPlaylistPaint = new Paint();
}
Canvas canvas = new Canvas(mPlaylistComposite);
final int numImages = mPlaylistSource.size();
final int compositeWidth = mPlaylistComposite.getWidth();
final int compositeHeight = mPlaylistComposite.getHeight();
if (numImages == 1) {
// If we expected only one image, return this result
if (mNumComposite == 1) {
mDone = true;
mHandler.removeCallbacks(mTimeoutWatchdog);
try {
mCallback.onArtLoaded(mPlaylistSource.get(0).getBitmap());
} catch (RemoteException ignored) {
}
return;
}
} else if (numImages == 2 || numImages == 3) {
int i = 0;
synchronized (mPlaylistSource) {
for (RecyclingBitmapDrawable item : mPlaylistSource) {
Bitmap itemBmp = item.getBitmap();
Rect src = new Rect(0, 0, itemBmp.getWidth(), itemBmp.getHeight());
Rect dst = new Rect(i * compositeWidth / numImages,
0,
i * compositeWidth / numImages + compositeWidth,
compositeHeight);
canvas.drawBitmap(itemBmp, src, dst, mPlaylistPaint);
++i;
}
}
} else {
for (int i = 0; i < 4; ++i) {
if (mPlaylistSource.size() > i) {
RecyclingBitmapDrawable item = mPlaylistSource.get(i);
Bitmap itemBmp = item.getBitmap();
int row = (int) Math.floor(i / 2);
int col = (i % 2);
Rect src = new Rect(0, 0, itemBmp.getWidth(), itemBmp.getHeight());
Rect dst = new Rect(col * compositeWidth / 2,
row * compositeHeight / 2,
col * compositeWidth / 2 + compositeWidth / 2,
row * compositeHeight / 2 + compositeHeight / 2);
canvas.drawBitmap(itemBmp, src, dst, mPlaylistPaint);
}
}
}
if (DEBUG) Log.d(TAG, "Got image " + numImages + "/" + mNumComposite);
if (numImages == mNumComposite) {
mDone = true;
mHandler.removeCallbacks(mTimeoutWatchdog);
try {
mCallback.onArtLoaded(mPlaylistComposite.copy(Bitmap.Config.ARGB_8888, false));
} catch (RemoteException ignored) {
}
}
}
public void start(Resources res, Playlist playlist, IArtCallback callback) {
if (DEBUG) Log.d(TAG, "Starting to build playlist art for " + playlist.getName());
if (playlist.getSongsCount() == 0) {
Log.d(TAG, "Playlist " + playlist.getName() + " has no tracks, skipping art building");
mDone = true;
try {
callback.onArtLoaded(null);
} catch (RemoteException ignore) {
}
return;
}
mDone = false;
mCallback = callback;
if (mCompositeTasks == null) {
mCompositeTasks = new ArrayList<>();
} else {
// Cancel the current tasks
for (AlbumArtTask task : mCompositeTasks) {
task.cancel(true);
}
mCompositeTasks.clear();
}
// Load 4 songs if possible and compose them into one picture
mPlaylistSource.clear();
mCompositeRequests = new ArrayList<>();
mNumComposite = Math.min(4, playlist.getSongsCount());
final ProviderAggregator aggregator = ProviderAggregator.getDefault();
for (int i = 0; i < mNumComposite; ++i) {
String entry = playlist.songsList().get(i);
Song song = aggregator.retrieveSong(entry, playlist.getProvider());
mCompositeRequests.add(song);
mCompositeTasks.add(AlbumArtHelper.retrieveAlbumArt(res, mCompositeListener, song, 600, false));
}
mHandler.postDelayed(mTimeoutWatchdog, 8000);
}
}