Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show cover art of songs not in albums #1122

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 90 additions & 59 deletions app/src/main/java/ch/blinkenlights/android/vanilla/CoverCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ public class CoverCache {
* Use vanilla musics INLINE cover load mechanism
*/
public static final int COVER_MODE_INLINE = 0x8;
/**
* The fallback cover image resource encoded as bitmap
*/
private static Bitmap sFallbackCover;
/**
* The large fallback cover image resource encoded as bitmap
*/
private static Bitmap sFallbackCoverLarge;
/**
* Shared on-disk cache class
*/
Expand All @@ -104,6 +112,12 @@ public CoverCache(Context context) {
if (sBitmapDiskCache == null) {
sBitmapDiskCache = new BitmapDiskCache(context.getApplicationContext(), 25*1024*1024);
}
if (sFallbackCover == null) {
sFallbackCover = BitmapFactory.decodeResource(context.getResources(), R.drawable.fallback_cover);
}
if (sFallbackCoverLarge == null) {
sFallbackCoverLarge = BitmapFactory.decodeResource(context.getResources(), R.drawable.fallback_cover_large);
}
}

/**
Expand All @@ -114,14 +128,17 @@ public CoverCache(Context context) {
* @return a bitmap or null if no artwork was found
*/
public Bitmap getCoverFromSong(Context ctx, Song song, int size) {
CoverKey key = new CoverCache.CoverKey(MediaUtils.TYPE_ALBUM, song.albumId, size);
CoverKey key;
if (MediaUtils.isSongInAlbum(song.flags))
key = new CoverCache.CoverKey(MediaUtils.TYPE_ALBUM, song.albumId, size);
else
key = new CoverCache.CoverKey(MediaUtils.TYPE_SONG, song.id, size);

Bitmap cover = getStoredCover(key);
if (cover == null) {
cover = sBitmapDiskCache.createBitmap(ctx, song, size*size);
if (cover != null) {
storeCover(key, cover);
cover = getStoredCover(key); // return lossy version to avoid random quality changes
}
storeCover(key, cover);
cover = getStoredCover(key); // return lossy version to avoid random quality changes
}
return cover;
}
Expand Down Expand Up @@ -347,19 +364,28 @@ public void put(CoverKey key, Bitmap cover) {
// Ensure that there is some space left
trim(mCacheSize);

ByteArrayOutputStream out = new ByteArrayOutputStream();
// We store a lossy version as this image was
// created from the original source (and will not be re-compressed)
cover.compress(Bitmap.CompressFormat.JPEG, 85, out);
ByteArrayOutputStream out = null;
if (cover != null) {
out = new ByteArrayOutputStream();
// We store a lossy version as this image was
// created from the original source (and will not be re-compressed)
cover.compress(Bitmap.CompressFormat.JPEG, 85, out);
}

Random rnd = new Random();
long ttl = getUnixTime() + rnd.nextInt(OBJECT_TTL);

ContentValues values = new ContentValues();
values.put("id" , key.hashCode());
values.put("expires", ttl);
values.put("size" , out.size());
values.put("blob" , out.toByteArray());
if (out != null) {
values.put("size", out.size());
values.put("blob", out.toByteArray());
}
else {
values.putNull("size");
values.putNull("blob");
}

dbh.insert(TABLE_NAME, null, values);
}
Expand All @@ -377,8 +403,8 @@ public Bitmap get(CoverKey key) {
String selection = "id=?";
String[] selectionArgs = { Long.toString(key.hashCode()) };
Cursor cursor = dbh.query(TABLE_NAME, FULL_PROJECTION, selection, selectionArgs, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
if (cursor != null && cursor.moveToFirst()) {
if (!cursor.isNull(3)) {
long expires = cursor.getLong(2);
byte[] blob = cursor.getBlob(3);

Expand All @@ -388,6 +414,8 @@ public Bitmap get(CoverKey key) {
ByteArrayInputStream stream = new ByteArrayInputStream(blob);
cover = BitmapFactory.decodeStream(stream);
}
} else {
cover = key.coverSize >= CoverCache.SIZE_MEDIUM ? sFallbackCoverLarge : sFallbackCover;
}
cursor.close();
}
Expand All @@ -411,63 +439,66 @@ public Bitmap createBitmap(Context ctx, Song song, long maxPxCount) {
InputStream inputStream = null;
InputStream sampleInputStream = null; // same as inputStream but used for getSampleSize

if ((CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_VANILLA) != 0) {
final File baseFile = new File(song.path); // File object of queried song
String bestMatchPath = null; // The best cover-path we found
int bestMatchIndex = COVER_MATCHES.length; // The best cover-index/priority found
int loopCount = 0; // Directory items loop counter

// Only start search if the base directory of this file is NOT the public
// downloads folder: Picking files from there would lead to a false positive
// in most cases
if (baseFile.getParentFile().equals(sDownloadsDir) == false) {
for (final File entry : baseFile.getParentFile().listFiles()) {
for (int i=0; i < bestMatchIndex ; i++) {
// We are checking each file entry to see if it matches a known
// cover pattern. We abort on first hit as the Pattern array is sorted from good->meh
if (COVER_MATCHES[i].matcher(entry.toString()).matches()) {
bestMatchIndex = i;
bestMatchPath = entry.toString();
break;
//If the song is not in an album, skip checking for art in albums
if (MediaUtils.isSongInAlbum(song.flags)) {
if ((CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_VANILLA) != 0) {
final File baseFile = new File(song.path); // File object of queried song
String bestMatchPath = null; // The best cover-path we found
int bestMatchIndex = COVER_MATCHES.length; // The best cover-index/priority found
int loopCount = 0; // Directory items loop counter

// Only start search if the base directory of this file is NOT the public
// downloads folder: Picking files from there would lead to a false positive
// in most cases
if (baseFile.getParentFile().equals(sDownloadsDir) == false) {
for (final File entry : baseFile.getParentFile().listFiles()) {
for (int i=0; i < bestMatchIndex ; i++) {
// We are checking each file entry to see if it matches a known
// cover pattern. We abort on first hit as the Pattern array is sorted from good->meh
if (COVER_MATCHES[i].matcher(entry.toString()).matches()) {
bestMatchIndex = i;
bestMatchPath = entry.toString();
break;
}
}
// Stop loop if we found the best match or if we looped 150 times
if (loopCount++ > 150 || bestMatchIndex == 0)
break;
}
}

if (bestMatchPath != null) {
final File guessedFile = new File(bestMatchPath);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
}
// Stop loop if we found the best match or if we looped 150 times
if (loopCount++ > 150 || bestMatchIndex == 0)
break;
}
}

if (bestMatchPath != null) {
final File guessedFile = new File(bestMatchPath);
if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_SHADOW) != 0) {
final String shadowBase = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + "/.vanilla";
final String shadowPath = shadowBase + "/" + (song.artist.replaceAll("/", "_"))+"/"+(song.album.replaceAll("/", "_"))+".jpg";

File guessedFile = new File(shadowPath);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
}
}
}

if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_SHADOW) != 0) {
final String shadowBase = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getPath() + "/.vanilla";
final String shadowPath = shadowBase + "/" + (song.artist.replaceAll("/", "_"))+"/"+(song.album.replaceAll("/", "_"))+".jpg";

File guessedFile = new File(shadowPath);
if (guessedFile.exists() && !guessedFile.isDirectory()) {
inputStream = new FileInputStream(guessedFile);
sampleInputStream = new FileInputStream(guessedFile);
}
}

if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_ANDROID) != 0) {
ContentResolver res = ctx.getContentResolver();
long[] androidIds = MediaUtils.getAndroidMediaIds(ctx, song);
long albumId = androidIds[1];

if (albumId != -1) {
// now we can query for the album art path if we found an album id
Uri uri = Uri.parse("content://media/external/audio/albumart/"+albumId);
sampleInputStream = res.openInputStream(uri);
if (sampleInputStream != null) // cache misses are VERY expensive here, so we check if the first open worked
inputStream = res.openInputStream(uri);
if (inputStream == null && (CoverCache.mCoverLoadMode & CoverCache.COVER_MODE_ANDROID) != 0) {
ContentResolver res = ctx.getContentResolver();
long[] androidIds = MediaUtils.getAndroidMediaIds(ctx, song);
long albumId = androidIds[1];

if (albumId != -1) {
// now we can query for the album art path if we found an album id
Uri uri = Uri.parse("content://media/external/audio/albumart/"+albumId);
sampleInputStream = res.openInputStream(uri);
if (sampleInputStream != null) // cache misses are VERY expensive here, so we check if the first open worked
inputStream = res.openInputStream(uri);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ public boolean handleMessage(Message message) {
// This message was sent due to a cache miss, but the cover might got cached in the meantime
Bitmap bitmap = sBitmapLruCache.get(payload.key);
if (bitmap == null) {
if (payload.key.mediaType == MediaUtils.TYPE_ALBUM) {
// We only display real covers for queries using the album id as key
if (payload.key.mediaType == MediaUtils.TYPE_ALBUM || payload.key.mediaType == MediaUtils.TYPE_SONG) {
// We only display real covers for queries using the album or song id as key
Song song = MediaUtils.getSongByTypeId(mContext, payload.key.mediaType, payload.key.mediaId);
if (song != null) {
bitmap = song.getSmallCover(mContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,6 @@ public MediaAdapter(Context context, int type, Limiter limiter, LibraryActivity
MediaLibrary.SongColumns.PATH+" %1$s",
MediaLibrary.SongColumns.DURATION+" %1$s",
};
// Songs covers are cached per-album
mCoverCacheType = MediaUtils.TYPE_ALBUM;
coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID;
break;
case MediaUtils.TYPE_PLAYLIST:
mSource = MediaLibrary.VIEW_PLAYLISTS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,12 @@ public static long getCurrentIdForType(Song song, int mType)
return TYPE_INVALID;
}
}

/**
* Check if a song's FLAG_NO_ALBUM flag is set
* @param songFlags the Song's flags
*/
public static boolean isSongInAlbum(int songFlags) {
return (songFlags & Song.FLAG_NO_ALBUM) == 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class PlaylistAdapter extends CursorAdapter implements Handler.Callback {
MediaLibrary.ContributorColumns.ARTIST,
MediaLibrary.SongColumns.ALBUM_ID,
MediaLibrary.SongColumns.DURATION,
MediaLibrary.SongColumns.FLAGS
};

private final Context mContext;
Expand Down Expand Up @@ -109,6 +110,7 @@ public void bindView(View view, Context context, Cursor cursor)
final String title = cursor.getString(2);
final String album = cursor.getString(3);
final String artist = cursor.getString(4);
final int flags = cursor.getInt(7);

ViewHolder holder = new ViewHolder();
holder.title = title;
Expand All @@ -121,7 +123,11 @@ public void bindView(View view, Context context, Cursor cursor)
dview.setDuration(cursor.getLong(6));

LazyCoverView cover = dview.getCoverView();
cover.setCover(MediaUtils.TYPE_ALBUM, cursor.getLong(5), null);

if ((flags & MediaLibrary.SONG_FLAG_NO_ALBUM) == 0)
cover.setCover(MediaUtils.TYPE_ALBUM, cursor.getLong(5), null);
else
cover.setCover(MediaUtils.TYPE_SONG, cursor.getLong(1), null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ public View getView(int position, View convertView, ViewGroup parent) {
if (song.isFilled()) {
row.setText(song.title, song.album+" · "+song.artist);
row.setDuration(song.duration);
row.getCoverView().setCover(MediaUtils.TYPE_ALBUM, song.albumId, null);
if (MediaUtils.isSongInAlbum(song.flags))
row.getCoverView().setCover(MediaUtils.TYPE_ALBUM, song.albumId, null);
else
row.getCoverView().setCover(MediaUtils.TYPE_SONG, song.id, null);
}

row.highlightRow(position == mHighlightRow);
Expand Down
25 changes: 10 additions & 15 deletions app/src/main/java/ch/blinkenlights/android/vanilla/Song.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ public class Song implements Comparable<Song> {
*/
public static final int FLAG_RANDOM = 0x1;
/**
* If set, this song has no cover art. If not set, this song may or may not
* have cover art.
* Indicates that this song does not belong to an album.
*/
public static final int FLAG_NO_COVER = 0x2;
public static final int FLAG_NO_ALBUM = 0x2;
/**
* The number of flags.
*/
Expand Down Expand Up @@ -135,7 +134,7 @@ public class Song implements Comparable<Song> {
public int discNumber;

/**
* Song flags. Currently {@link #FLAG_RANDOM} or {@link #FLAG_NO_COVER}.
* Song flags. Currently {@link #FLAG_RANDOM}, {@link #FLAG_NO_ALBUM}.
*/
public int flags;

Expand Down Expand Up @@ -198,7 +197,7 @@ public void populate(Cursor cursor)
if ((libraryFlags & MediaLibrary.SONG_FLAG_NO_ALBUM) != 0) {
// Note that we only set, never unset: the song may already
// have the flag set for other reasons.
flags |= FLAG_NO_COVER;
flags |= FLAG_NO_ALBUM;
}
}

Expand Down Expand Up @@ -230,7 +229,7 @@ public String getTrackAndDiscNumber() {
* Query the large album art for this song.
*
* @param context A context to use.
* @return The album art or null if no album art could be found
* @return The album art or the fallback art if no album art could be found
*/
public Bitmap getLargeCover(Context context) {
return getCoverInternal(context, CoverCache.SIZE_LARGE);
Expand All @@ -240,7 +239,7 @@ public Bitmap getLargeCover(Context context) {
* Query the medium album art for this song.
*
* @param context A context to use.
* @return The album art or null if no album art could be found
* @return The album art or the fallback art if no album art could be found
*/
public Bitmap getMediumCover(Context context) {
return getCoverInternal(context, CoverCache.SIZE_MEDIUM);
Expand All @@ -250,7 +249,7 @@ public Bitmap getMediumCover(Context context) {
* Query the small album art for this song.
*
* @param context A context to use.
* @return The album art or null if no album art could be found
* @return The album art or the fallback art if no album art could be found
*/
public Bitmap getSmallCover(Context context) {
return getCoverInternal(context, CoverCache.SIZE_SMALL);
Expand All @@ -261,20 +260,16 @@ public Bitmap getSmallCover(Context context) {
*
* @param context A context to use.
* @param size The desired cover size
* @return The album art or null if no album art could be found
* @return The album art or the fallback art if no album art could be found
*/
private Bitmap getCoverInternal(Context context, int size) {
if (CoverCache.mCoverLoadMode == 0 || id <= -1 || (flags & FLAG_NO_COVER) != 0)
if (CoverCache.mCoverLoadMode == 0 || id <= -1)
return null;

if (sCoverCache == null)
sCoverCache = new CoverCache(context.getApplicationContext());

Bitmap cover = sCoverCache.getCoverFromSong(context, this, size);

if (cover == null)
flags |= FLAG_NO_COVER;
return cover;
return sCoverCache.getCoverFromSong(context, this, size);
}

@Override
Expand Down