Use LruCache for cover art and no cache in CoverView

This commit is contained in:
Christopher Eby 2012-02-15 22:37:04 -06:00
parent 623a1b2332
commit 5d086bb82d
4 changed files with 98 additions and 267 deletions

View File

@ -1,161 +0,0 @@
/*
* Copyright (C) 2010 Christopher Eby <kreed@kreed.org>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.kreed.vanilla;
import junit.framework.Assert;
import java.util.Arrays;
/**
* A key/value map that discards old items. When the capacity of the cache is
* reached, the oldest item (by insertion time) will be discarded.
*
* Keys should be non-negative.
*
* @param <E> The type of the values. (Keys will be longs).
*/
public class Cache<E> {
/**
* The keys contained in the cache, stored in the order of insertion.
*/
private final long[] mKeys;
/**
* The values contained in the cache, stored in a location corresponding
* to the keys in mKeys.
*/
private final Object[] mValues;
/**
* Create a Cache.
*
* @param capacity The capacity of the cache. This is fixed and may not be
* changed after construction.
*/
public Cache(int capacity)
{
mKeys = new long[capacity];
mValues = new Object[capacity];
Arrays.fill(mKeys, -1);
}
/**
* Calculate the number of items in the cache.
*
* @return The number of items in the cache.
*/
private int count()
{
long[] keys = mKeys;
int count = keys.length;
while (--count != -1 && keys[count] == -1);
return count + 1;
}
/**
* Find the index of the given key.
*
* @param key The key to search for.
* @return The index, or -1 if the key was not found.
*/
private int indexOf(long key)
{
long[] keys = mKeys;
for (int i = keys.length; --i != -1; )
if (keys[i] == key)
return i;
return -1;
}
/**
* Retrieve the value with the given key.
*
* @param key The key to search with.
* @return The value, or null if the given key is not contained in this
* cache.
*/
@SuppressWarnings("unchecked")
public E get(long key)
{
int i = indexOf(key);
return i == -1 ? null : (E)mValues[i];
}
/**
* Reset the age of the item with the given key so that it will be
* discarded last.
*
* @param key The key of the item to touch.
*/
public synchronized void touch(long key)
{
long[] keys = mKeys;
Object[] values = mValues;
int oldPos = indexOf(key);
int newPos = count() - 1;
if (oldPos != newPos && oldPos != -1) {
Object value = values[oldPos];
System.arraycopy(keys, oldPos + 1, keys, oldPos, newPos - oldPos);
System.arraycopy(values, oldPos + 1, values, oldPos, newPos - oldPos);
keys[newPos] = key;
values[newPos] = value;
}
}
/**
* Discard the oldest item in the cache. Does nothing if the cache is not
* full.
*
* @return The item that was discarded, or null if the cache is not full.
*/
@SuppressWarnings("unchecked")
public synchronized E discardOldest()
{
int count = count();
// Cache is not full.
if (count != mKeys.length)
return null;
E removed = (E)mValues[0];
System.arraycopy(mKeys, 1, mKeys, 0, mKeys.length - 1);
System.arraycopy(mValues, 1, mValues, 0, mKeys.length - 1);
mKeys[mKeys.length - 1] = -1;
return removed;
}
/**
* Insert an item into the cache. Cache must not be full.
*
* @param key The key to place the item at. Must be a duplicate of an
* existing key in the cache.
* @param value The item.
*/
public synchronized void put(long key, E value)
{
int count = count();
Assert.assertFalse(count == mKeys.length); // must not be full
mKeys[count] = key;
mValues[count] = value;
}
}

View File

@ -27,7 +27,6 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
@ -121,27 +120,24 @@ public final class CoverBitmap {
* @param song Title and other data are taken from here for info modes.
* @param width Maximum width of image
* @param height Maximum height of image
* @param reuse A Bitmap to be drawn into. If null, a new Bitmap will be
* created. If the bitmap cannot be used, it will be recycled and a new
* Bitmap created.
* @return The image, or null if the song was null, or width or height
* were less than 1
*/
public static Bitmap createBitmap(Context context, int style, Bitmap coverArt, Song song, int width, int height, Bitmap reuse)
public static Bitmap createBitmap(Context context, int style, Bitmap coverArt, Song song, int width, int height)
{
switch (style) {
case STYLE_OVERLAPPING_BOX:
return createOverlappingBitmap(context, coverArt, song, width, height, reuse);
return createOverlappingBitmap(context, coverArt, song, width, height);
case STYLE_INFO_BELOW:
return createSeparatedBitmap(context, coverArt, song, width, height, reuse);
return createSeparatedBitmap(context, coverArt, song, width, height);
case STYLE_NO_INFO:
return createScaledBitmap(coverArt, width, height, reuse);
return createScaledBitmap(coverArt, width, height);
default:
throw new IllegalArgumentException("Invalid bitmap type given: " + style);
}
}
private static Bitmap createOverlappingBitmap(Context context, Bitmap cover, Song song, int width, int height, Bitmap bitmap)
private static Bitmap createOverlappingBitmap(Context context, Bitmap cover, Song song, int width, int height)
{
if (TEXT_SIZE == -1)
loadTextSizes(context);
@ -185,17 +181,7 @@ public final class CoverBitmap {
int bitmapWidth = Math.max(coverWidth, boxWidth);
int bitmapHeight = Math.max(coverHeight, boxHeight);
if (bitmap != null) {
if (bitmap.getHeight() != bitmapHeight || bitmap.getWidth() != bitmapWidth) {
bitmap.recycle();
bitmap = null;
} else {
bitmap.eraseColor(Color.BLACK);
}
}
if (bitmap == null)
bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
if (cover != null) {
@ -231,7 +217,7 @@ public final class CoverBitmap {
return bitmap;
}
private static Bitmap createSeparatedBitmap(Context context, Bitmap cover, Song song, int width, int height, Bitmap bitmap)
private static Bitmap createSeparatedBitmap(Context context, Bitmap cover, Song song, int width, int height)
{
if (TEXT_SIZE == -1)
loadTextSizes(context);
@ -281,17 +267,7 @@ public final class CoverBitmap {
int bitmapWidth = horizontal ? coverWidth + boxWidth : Math.max(coverWidth, boxWidth);
int bitmapHeight = horizontal ? Math.max(coverHeight, boxHeight) : coverHeight + boxHeight;
if (bitmap != null) {
if (bitmap.getHeight() != bitmapHeight || bitmap.getWidth() != bitmapWidth) {
bitmap.recycle();
bitmap = null;
} else {
bitmap.eraseColor(Color.BLACK);
}
}
if (bitmap == null)
bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
if (cover != null) {
@ -334,18 +310,13 @@ public final class CoverBitmap {
* preserved. At least one dimension of the result will match the provided
* dimension exactly.
*
* @param source The source bitmap. Will be recycled.
* @param source The bitmap to be scaled
* @param width Maximum width of image
* @param height Maximum height of image
* @param reuse A bitmap that will simply be recycled. (This method does not
* support reuse.)
* @return The scaled bitmap.
*/
private static Bitmap createScaledBitmap(Bitmap source, int width, int height, Bitmap reuse)
private static Bitmap createScaledBitmap(Bitmap source, int width, int height)
{
if (reuse != null)
reuse.recycle();
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
float scale = Math.min((float)width / sourceWidth, (float)height / sourceHeight);

View File

@ -91,15 +91,11 @@ public final class CoverView extends View implements Handler.Callback {
/**
* The current set of songs: 0 = previous, 1 = current, and 2 = next.
*/
private final Song[] mSongs = new Song[3];
private Song[] mSongs = new Song[3];
/**
* The covers for the current songs: 0 = previous, 1 = current, and 2 = next.
*/
private final Bitmap[] mBitmaps = new Bitmap[3];
/**
* Cache of cover bitmaps generated for songs. The song ids are the keys.
*/
private final Cache<Bitmap> mBitmapCache = new Cache<Bitmap>(8);
private Bitmap[] mBitmaps = new Bitmap[3];
/**
* Cover art to use when a song has no cover art in no info display styles.
*/
@ -331,38 +327,28 @@ public final class CoverView extends View implements Handler.Callback {
}
/**
* Generates a bitmap for the given song if the cache does not contain one
* for it, or moves the bitmap to the top of the cache if it does.
* Generates a bitmap for the given song.
*
* @param i The position of the song in mSongs.
*/
private void generateBitmap(int i)
{
Song song = mSongs[i];
if (song == null || song.id == -1)
return;
Bitmap reuse = mBitmapCache.discardOldest();
if (reuse == mDefaultCover)
reuse = null;
int style = mCoverStyle;
Context context = getContext();
Bitmap cover = song.getCover(context);
int width = getWidth();
int height = getHeight();
Bitmap cover = song == null ? null : song.getCover(context);
Bitmap bitmap;
if (cover == null && style == CoverBitmap.STYLE_NO_INFO) {
if (mDefaultCover == null)
mDefaultCover = CoverBitmap.generateDefaultCover(width, height);
bitmap = mDefaultCover;
Bitmap def = mDefaultCover;
if (def == null) {
mDefaultCover = def = CoverBitmap.generateDefaultCover(getWidth(), getHeight());
}
mBitmaps[i] = def;
} else {
bitmap = CoverBitmap.createBitmap(context, style, cover, song, width, height, reuse);
mBitmaps[i] = CoverBitmap.createBitmap(context, style, cover, song, getWidth(), getHeight());
}
mBitmaps[i] = bitmap;
mBitmapCache.put(song.id, bitmap);
postInvalidate();
}
@ -372,18 +358,13 @@ public final class CoverView extends View implements Handler.Callback {
*/
public void setSong(int i, Song song)
{
if (song == mSongs[i])
return;
mSongs[i] = song;
if (song == null) {
mBitmaps[i] = null;
} else {
Bitmap bitmap = mBitmapCache.get(song.id);
if (bitmap != null) {
mBitmaps[i] = bitmap;
mBitmapCache.touch(song.id);
} else {
mBitmaps[i] = null;
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0));
}
mBitmaps[i] = null;
if (song != null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0));
}
}
@ -400,9 +381,30 @@ public final class CoverView extends View implements Handler.Callback {
}
mHandler.removeMessages(MSG_GENERATE_BITMAP);
setSong(1, service.getSong(0));
setSong(2, service.getSong(1));
setSong(0, service.getSong(-1));
Song[] songs = mSongs;
Bitmap[] bitmaps = mBitmaps;
Song[] newSongs = { service.getSong(-1), service.getSong(0), service.getSong(1) };
Bitmap[] newBitmaps = new Bitmap[3];
mSongs = newSongs;
mBitmaps = newBitmaps;
for (int i = 0; i != 3; ++i) {
if (newSongs[i] == null)
continue;
for (int j = 0; j != 3; ++j) {
if (newSongs[i] == songs[j]) {
newBitmaps[i] = bitmaps[j];
break;
}
}
if (newBitmaps[i] == null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_GENERATE_BITMAP, i, 0));
}
}
resetScroll();
invalidate();
}
@ -410,7 +412,7 @@ public final class CoverView extends View implements Handler.Callback {
/**
* Call {@link CoverView#generateBitmap(int)} for the song at the given index.
*
* obj must be the Song to generate a bitmap for.
* arg1 should be the index of the song.
*/
private static final int MSG_GENERATE_BITMAP = 0;
/**

View File

@ -30,7 +30,7 @@ import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import android.support.v4.util.LruCache;
import java.io.FileDescriptor;
/**
@ -85,9 +85,47 @@ public class Song implements Comparable<Song> {
};
/**
* A cache of 8 covers.
* A cache of 6 MiB of covers.
*/
private static final Cache<Bitmap> sCoverCache = new Cache<Bitmap>(8);
private static class CoverCache extends LruCache<Long, Bitmap> {
private final Context mContext;
public CoverCache(Context context)
{
super(6 * 1024 * 1024);
mContext = context;
}
@Override
public Bitmap create(Long key)
{
Uri uri = Uri.parse("content://media/external/audio/media/" + key + "/albumart");
ContentResolver res = mContext.getContentResolver();
try {
ParcelFileDescriptor parcelFileDescriptor = res.openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null) {
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, BITMAP_OPTIONS);
}
} catch (Exception e) {
// no cover art found
}
return null;
}
@Override
protected int sizeOf(Long key, Bitmap value)
{
return value.getRowBytes() * value.getHeight();
}
};
/**
* The cache instance.
*/
private static CoverCache sCoverCache = null;
/**
* If true, will not attempt to load any cover art in getCover()
@ -215,32 +253,13 @@ public class Song implements Comparable<Song> {
if (mDisableCoverArt || id == -1 || (flags & FLAG_NO_COVER) != 0)
return null;
if (sCoverCache == null)
sCoverCache = new CoverCache(context.getApplicationContext());
Bitmap cover = sCoverCache.get(id);
if (cover != null) {
return cover;
}
Uri uri = Uri.parse("content://media/external/audio/media/" + id + "/albumart");
ContentResolver res = context.getContentResolver();
try {
ParcelFileDescriptor parcelFileDescriptor = res.openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null) {
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
cover = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, BITMAP_OPTIONS);
if (cover != null) {
Bitmap discarded = sCoverCache.discardOldest();
if (discarded != null)
discarded.recycle();
sCoverCache.put(id, cover);
return cover;
}
}
} catch (Exception e) {
Log.d("VanillaMusic", "Failed to load cover art for " + path, e);
}
flags |= FLAG_NO_COVER;
return null;
if (cover == null)
flags |= FLAG_NO_COVER;
return cover;
}
@Override