diff --git a/res/layout/library_row_expandable.xml b/res/layout/library_row_expandable.xml index 14909173..6277b69d 100644 --- a/res/layout/library_row_expandable.xml +++ b/res/layout/library_row_expandable.xml @@ -23,6 +23,15 @@ THE SOFTWARE. + + * + * 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 . + */ + +package ch.blinkenlights.android.vanilla; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import android.util.AttributeSet; +import android.widget.ImageView; +import android.util.LruCache; + +/** + * LazyCoverView implements a 'song-aware' ImageView + * + * View updates should be triggered via setCover(type, id) to + * instruct the view to load the cover from its own LRU cache. + * + * The cover will automatically be fetched & scaled in a background + * thread on cache miss + */ +public class LazyCoverView extends ImageView + implements Handler.Callback +{ + /** + * Context of constructor + */ + private Context mContext; + /** + * UI Thread handler + */ + private static Handler sUiHandler; + /** + * Worker thread handler + */ + private static Handler sHandler; + /** + * The fallback cover image resource encoded as bitmap + */ + private static Bitmap sFallbackBitmap; + /** + * Bitmap LRU cache + */ + private static BitmapCache sBitmapCache; + /** + * The key we are expected to draw + */ + private String mExpectedKey; + /** + * Dimension of cached pictures in pixels + */ + private int mImageDimensionPx; + /** + * Bitmap cache LRU cache implementation + */ + private class BitmapCache extends LruCache { + public BitmapCache(int size) { + super(size); + } + } + /** + * Cover message we are passing around using mHandler + */ + private static class CoverMsg { + public int type; // Media type + public long id; // ID of this media type to query + public LazyCoverView view; // The view we are updating + CoverMsg(int type, long id, LazyCoverView view) { + this.type = type; + this.id = id; + this.view = view; + } + public String getKey() { + return this.type +"/"+ this.id; + } + /** + * Returns true if the view still requires updating + */ + public boolean isRecent() { + return this.getKey().equals(this.view.mExpectedKey); + } + } + + /** + * Constructor of class inflated from XML + * + * @param context The context of the calling activity + * @param attributes attributes passed by the xml inflater + */ + public LazyCoverView(Context context, AttributeSet attributes) { + super(context, attributes); + mContext = context; + } + + /** + * Setup the handler of this view instance. This function + * must be called before calling setCover(). + * + * @param looper The worker thread to use for image scaling + */ + public void setup(Looper looper) { + if (sBitmapCache == null) { + sBitmapCache = new BitmapCache(255); // Cache up to 255 items + } + if (sFallbackBitmap == null) { + sFallbackBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.fallback_cover); + } + if (sUiHandler == null) { + sUiHandler = new Handler(this); + } + if (sHandler == null) { + sHandler = new Handler(looper, this); + } + // image dimension we are going to cache - we should probably calculate this + mImageDimensionPx = 128; + } + + + /** + * mHandler and mUiHandler callbacks + */ + private static final int MSG_CACHE_COVER = 60; + private static final int MSG_DRAW_COVER = 61; + + @Override + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_CACHE_COVER: { + CoverMsg payload = (CoverMsg)message.obj; + + if (payload.isRecent() == false) { + // This RPC is already obsoleted: drop it + break; + } + + Bitmap bitmap = sBitmapCache.get(payload.getKey()); + if (bitmap == null) { + Song song = MediaUtils.getSongByTypeId(mContext.getContentResolver(), payload.type, payload.id); + if (song != null) { + bitmap = song.getCover(mContext); + } + if (bitmap == null) { + bitmap = sFallbackBitmap; + } + bitmap = Bitmap.createScaledBitmap(bitmap, mImageDimensionPx, mImageDimensionPx, true); + sBitmapCache.put(payload.getKey(), bitmap); + } + sUiHandler.sendMessage(sUiHandler.obtainMessage(MSG_DRAW_COVER, payload)); + break; + } + case MSG_DRAW_COVER: { + CoverMsg payload = (CoverMsg)message.obj; + synchronized(payload.view) { + if (payload.isRecent()) { + drawFromCache(payload); + } + } + } + default: + return false; + } + return true; + } + + /** + * Attempts to set the image of this cover + * + * @param type The Media type + * @param id The id of this media type to query + */ + public void setCover(int type, long id) { + CoverMsg payload = new CoverMsg(type, id, this); + mExpectedKey = payload.getKey(); + if (drawFromCache(payload) == false) { + // We send this delayed to avoid a cache-miss-storm if the user scrolls + // quickly in the listview + sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_CACHE_COVER, payload), 200); + } + } + + /** + * Updates the view with a cached bitmap + * A fallback image will be used on cache miss + * + * @param payload The cover message containing the cache key and view to use + */ + public boolean drawFromCache(CoverMsg payload) { + boolean cacheHit = true; + Bitmap bitmap = sBitmapCache.get(payload.getKey()); + if (bitmap == null) { + cacheHit = false; + bitmap = sFallbackBitmap; + } + payload.view.setImageBitmap(bitmap); + return cacheHit; + } + +} diff --git a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index 39704150..67e31b31 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -309,29 +309,30 @@ public class LibraryPagerAdapter LayoutInflater inflater = activity.getLayoutInflater(); LibraryAdapter adapter; LinearLayout header = null; + Looper looper = mWorkerHandler.getLooper(); switch (type) { case MediaUtils.TYPE_ARTIST: - adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null); + adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null, looper); mArtistAdapter.setExpandable(mSongsPosition != -1 || mAlbumsPosition != -1); mArtistHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null); break; case MediaUtils.TYPE_ALBUM: - adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter); + adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter, looper); mAlbumAdapter.setExpandable(mSongsPosition != -1); mPendingAlbumLimiter = null; mAlbumHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null); break; case MediaUtils.TYPE_SONG: - adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter); + adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter, looper); mPendingSongLimiter = null; mSongHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row_expandable, null); break; case MediaUtils.TYPE_PLAYLIST: - adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null); + adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null, looper); break; case MediaUtils.TYPE_GENRE: - adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null); + adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null, looper); mGenreAdapter.setExpandable(mSongsPosition != -1); break; case MediaUtils.TYPE_FILE: diff --git a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java index cb18a301..37a2093b 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java @@ -28,6 +28,7 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.graphics.Color; import android.net.Uri; +import android.os.Looper; import android.provider.BaseColumns; import android.provider.MediaStore; import android.text.Spannable; @@ -76,6 +77,7 @@ public class MediaAdapter * The current data. */ private Cursor mCursor; + private Looper mLooper; /** * The type of media represented by this adapter. Must be one of the * MediaUtils.FIELD_* constants. Determines which content provider to query for @@ -134,6 +136,10 @@ public class MediaAdapter * If true, show the expander button on each row. */ private boolean mExpandable; + /** + * If true, return views with covers and fire callbacks + */ + private boolean mHasCoverArt; /** * Construct a MediaAdapter representing the given type of @@ -145,12 +151,13 @@ public class MediaAdapter * and what fields to display in the views. * @param limiter An initial limiter to use */ - public MediaAdapter(LibraryActivity activity, int type, Limiter limiter) + public MediaAdapter(LibraryActivity activity, int type, Limiter limiter, Looper looper) { mActivity = activity; mType = type; mLimiter = limiter; mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mLooper = looper; switch (type) { case MediaUtils.TYPE_ARTIST: @@ -169,6 +176,7 @@ public class MediaAdapter mSongSort = MediaUtils.ALBUM_SORT; mSortEntries = new int[] { R.string.name, R.string.artist_album, R.string.year, R.string.number_of_tracks, R.string.date_added }; mSortValues = new String[] { "album_key %1$s", "artist_key %1$s,album_key %1$s", "minyear %1$s,album_key %1$s", "numsongs %1$s,album_key %1$s", "_id %1$s" }; + mHasCoverArt = true; break; case MediaUtils.TYPE_SONG: mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; @@ -178,6 +186,7 @@ public class MediaAdapter R.string.artist_year, R.string.album_track, R.string.year, R.string.date_added, R.string.song_playcount }; mSortValues = new String[] { "title_key %1$s", "artist_key %1$s,album_key %1$s,track %1$s", "artist_key %1$s,album_key %1$s,title_key %1$s", "artist_key %1$s,year %1$s,track %1$s", "album_key %1$s,track %1s", "year %1$s,title_key %1$s", "_id %1$s", SORT_MAGIC_PLAYCOUNT }; + mHasCoverArt = true; break; case MediaUtils.TYPE_PLAYLIST: mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; @@ -448,10 +457,14 @@ public class MediaAdapter holder.text = (TextView)view.findViewById(R.id.text); holder.arrow = (ImageView)view.findViewById(R.id.arrow); + holder.cover = (LazyCoverView)view.findViewById(R.id.cover); holder.arrow.setOnClickListener(this); holder.text.setOnClickListener(this); + holder.cover.setOnClickListener(this); holder.arrow.setVisibility(mExpandable ? View.VISIBLE : View.GONE); + holder.cover.setVisibility(mHasCoverArt ? View.VISIBLE : View.GONE); + holder.cover.setup(mLooper); } else { holder = (ViewHolder)view.getTag(); } @@ -477,6 +490,10 @@ public class MediaAdapter holder.title = title; } + if (mHasCoverArt) { + holder.cover.setCover(mType, holder.id); + } + return view; } diff --git a/src/ch/blinkenlights/android/vanilla/ViewHolder.java b/src/ch/blinkenlights/android/vanilla/ViewHolder.java index 954d47c3..b39166e0 100644 --- a/src/ch/blinkenlights/android/vanilla/ViewHolder.java +++ b/src/ch/blinkenlights/android/vanilla/ViewHolder.java @@ -26,4 +26,5 @@ public class ViewHolder { public String title; public TextView text; public ImageView arrow; + public LazyCoverView cover; }