From edc4abb1f4502f02d1256128cf5c2e489dca0b6a Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Wed, 5 Aug 2015 14:05:26 +0200 Subject: [PATCH 01/41] library_row isnt a special snowflake anymore --- res/layout/library_row.xml | 21 +++++++---- .../android/vanilla/LibraryPagerAdapter.java | 36 ++++++++++--------- .../android/vanilla/MediaAdapter.java | 9 ++--- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/res/layout/library_row.xml b/res/layout/library_row.xml index 88855ebb..b89f85d2 100644 --- a/res/layout/library_row.xml +++ b/res/layout/library_row.xml @@ -20,11 +20,18 @@ 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. --> - + android:orientation="horizontal"> + + diff --git a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index a1f63143..0fdbcd26 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -43,6 +43,7 @@ import android.util.LruCache; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; +import android.widget.LinearLayout; import java.util.Arrays; /** @@ -53,7 +54,7 @@ public class LibraryPagerAdapter implements Handler.Callback , ViewPager.OnPageChangeListener , View.OnCreateContextMenuListener - , AdapterView.OnItemClickListener + , View.OnClickListener { /** * The number of unique list types. The number of visible lists may be @@ -162,9 +163,9 @@ public class LibraryPagerAdapter * song limiters. */ private String mHeaderText; - private TextView mArtistHeader; - private TextView mAlbumHeader; - private TextView mSongHeader; + private LinearLayout mArtistHeader; + private LinearLayout mAlbumHeader; + private LinearLayout mSongHeader; /** * The current filter text, or null if none. */ @@ -307,24 +308,24 @@ public class LibraryPagerAdapter LibraryActivity activity = mActivity; LayoutInflater inflater = activity.getLayoutInflater(); LibraryAdapter adapter; - TextView header = null; + LinearLayout header = null; switch (type) { case MediaUtils.TYPE_ARTIST: adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null); mArtistAdapter.setExpandable(mSongsPosition != -1 || mAlbumsPosition != -1); - mArtistHeader = header = (TextView)inflater.inflate(R.layout.library_row, null); + mArtistHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, null); break; case MediaUtils.TYPE_ALBUM: adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter); mAlbumAdapter.setExpandable(mSongsPosition != -1); mPendingAlbumLimiter = null; - mAlbumHeader = header = (TextView)inflater.inflate(R.layout.library_row, null); + mAlbumHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, null); break; case MediaUtils.TYPE_SONG: adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter); mPendingSongLimiter = null; - mSongHeader = header = (TextView)inflater.inflate(R.layout.library_row, null); + mSongHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, null); break; case MediaUtils.TYPE_PLAYLIST: adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null); @@ -343,10 +344,12 @@ public class LibraryPagerAdapter view = (ListView)inflater.inflate(R.layout.listview, null); view.setOnCreateContextMenuListener(this); - view.setOnItemClickListener(this); + view.setTag(type); if (header != null) { - header.setText(mHeaderText); + TextView headerText = (TextView)header.findViewById(R.id.text); + headerText.setText(mHeaderText); + headerText.setOnClickListener(this); header.setTag(type); view.addHeaderView(header); } @@ -466,11 +469,11 @@ public class LibraryPagerAdapter public void setHeaderText(String text) { if (mArtistHeader != null) - mArtistHeader.setText(text); + ((TextView)mArtistHeader.findViewById(R.id.text)).setText(text); if (mAlbumHeader != null) - mAlbumHeader.setText(text); + ((TextView)mAlbumHeader.findViewById(R.id.text)).setText(text); if (mSongHeader != null) - mSongHeader.setText(text); + ((TextView)mSongHeader.findViewById(R.id.text)).setText(text); mHeaderText = text; } @@ -836,13 +839,12 @@ public class LibraryPagerAdapter } @Override - public void onItemClick(AdapterView list, View view, int position, long id) - { - Intent intent = id == -1 ? createHeaderIntent(view) : mCurrentAdapter.createData(view); + public void onClick(View view) { + view = (View)view.getParent(); // get view of linear layout, not the click consumer + Intent intent = createHeaderIntent(view); mActivity.onItemClicked(intent); } - /** * LRU implementation for filebrowser position cache */ diff --git a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java index aa59af31..c3d846cb 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java @@ -454,15 +454,11 @@ public class MediaAdapter holder = new ViewHolder(); view.setTag(holder); + holder.text = (TextView)view.findViewById(R.id.text); if (mExpandable) { - holder.text = (TextView)view.findViewById(R.id.text); holder.arrow = (ImageView)view.findViewById(R.id.arrow); holder.arrow.setOnClickListener(this); - } else { - holder.text = (TextView)view; - view.setLongClickable(true); } - holder.text.setOnClickListener(this); } else { holder = (ViewHolder)view.getTag(); @@ -565,8 +561,7 @@ public class MediaAdapter public void onClick(View view) { int id = view.getId(); - if (mExpandable) - view = (View)view.getParent(); + view = (View)view.getParent(); // get view of linear layout, not the click consumer Intent intent = createData(view); if (id == R.id.arrow) { mActivity.onItemExpanded(intent); From 8d2df9b8a93a008a754b7271cbd4774aec7ca742 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Wed, 5 Aug 2015 17:58:50 +0200 Subject: [PATCH 02/41] simplify views --- res/layout/library_row.xml | 37 ------------------- res/layout/library_row_expandable.xml | 1 + .../android/vanilla/FileSystemAdapter.java | 13 +------ .../android/vanilla/LibraryPagerAdapter.java | 6 +-- .../android/vanilla/MediaAdapter.java | 20 +++------- .../android/vanilla/ViewHolder.java | 29 +++++++++++++++ 6 files changed, 41 insertions(+), 65 deletions(-) delete mode 100644 res/layout/library_row.xml create mode 100644 src/ch/blinkenlights/android/vanilla/ViewHolder.java diff --git a/res/layout/library_row.xml b/res/layout/library_row.xml deleted file mode 100644 index b89f85d2..00000000 --- a/res/layout/library_row.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - diff --git a/res/layout/library_row_expandable.xml b/res/layout/library_row_expandable.xml index e8849e33..14909173 100644 --- a/res/layout/library_row_expandable.xml +++ b/res/layout/library_row_expandable.xml @@ -47,5 +47,6 @@ THE SOFTWARE. android:src="@drawable/arrow" android:layout_width="44dip" android:layout_height="44dip" + android:visibility="gone" android:contentDescription="@string/expand" /> diff --git a/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java b/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java index 8b031c61..3de7da58 100644 --- a/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java @@ -188,13 +188,6 @@ public class FileSystemAdapter return pos; } - private static class ViewHolder { - public int id; - public TextView text; - public View divider; - public ImageView arrow; - } - @Override public View getView(int pos, View convertView, ViewGroup parent) { @@ -205,7 +198,6 @@ public class FileSystemAdapter view = mInflater.inflate(R.layout.library_row_expandable, null); holder = new ViewHolder(); holder.text = (TextView)view.findViewById(R.id.text); - holder.divider = view.findViewById(R.id.divider); holder.arrow = (ImageView)view.findViewById(R.id.arrow); holder.text.setOnClickListener(this); holder.arrow.setOnClickListener(this); @@ -219,7 +211,6 @@ public class FileSystemAdapter boolean isDirectory = file.isDirectory(); holder.id = pos; holder.text.setText(file.getName()); - holder.divider.setVisibility(isDirectory ? View.VISIBLE : View.GONE); holder.arrow.setVisibility(isDirectory ? View.VISIBLE : View.GONE); holder.text.setCompoundDrawablesWithIntrinsicBounds(isDirectory ? mFolderIcon : null, null, null, null); return view; @@ -295,11 +286,11 @@ public class FileSystemAdapter public Intent createData(View view) { ViewHolder holder = (ViewHolder)view.getTag(); - File file = mFiles[holder.id]; + File file = mFiles[(int)holder.id]; Intent intent = new Intent(); intent.putExtra(LibraryAdapter.DATA_TYPE, MediaUtils.TYPE_FILE); - intent.putExtra(LibraryAdapter.DATA_ID, (long)holder.id); + intent.putExtra(LibraryAdapter.DATA_ID, holder.id); intent.putExtra(LibraryAdapter.DATA_TITLE, holder.text.getText().toString()); intent.putExtra(LibraryAdapter.DATA_EXPANDABLE, file.isDirectory()); diff --git a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index 0fdbcd26..cf2e2af7 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -314,18 +314,18 @@ public class LibraryPagerAdapter case MediaUtils.TYPE_ARTIST: adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, null); mArtistAdapter.setExpandable(mSongsPosition != -1 || mAlbumsPosition != -1); - mArtistHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, null); + 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); mAlbumAdapter.setExpandable(mSongsPosition != -1); mPendingAlbumLimiter = null; - mAlbumHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, 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); mPendingSongLimiter = null; - mSongHeader = header = (LinearLayout)inflater.inflate(R.layout.library_row, 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); diff --git a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java index c3d846cb..cb18a301 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java @@ -433,33 +433,25 @@ public class MediaAdapter } } - private static class ViewHolder { - public long id; - public String title; - public TextView text; - public ImageView arrow; - } - @Override public View getView(int position, View view, ViewGroup parent) { ViewHolder holder; - if (view == null || mExpandable != view instanceof LinearLayout) { + if (view == null) { // We must create a new view if we're not given a recycle view or // if the recycle view has the wrong layout. - int layout = mExpandable ? R.layout.library_row_expandable : R.layout.library_row; - view = mInflater.inflate(layout, null); + view = mInflater.inflate(R.layout.library_row_expandable, null); holder = new ViewHolder(); view.setTag(holder); holder.text = (TextView)view.findViewById(R.id.text); - if (mExpandable) { - holder.arrow = (ImageView)view.findViewById(R.id.arrow); - holder.arrow.setOnClickListener(this); - } + holder.arrow = (ImageView)view.findViewById(R.id.arrow); + holder.arrow.setOnClickListener(this); holder.text.setOnClickListener(this); + + holder.arrow.setVisibility(mExpandable ? View.VISIBLE : View.GONE); } else { holder = (ViewHolder)view.getTag(); } diff --git a/src/ch/blinkenlights/android/vanilla/ViewHolder.java b/src/ch/blinkenlights/android/vanilla/ViewHolder.java new file mode 100644 index 00000000..954d47c3 --- /dev/null +++ b/src/ch/blinkenlights/android/vanilla/ViewHolder.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Adrian Ulrich + * + * 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.widget.TextView; +import android.widget.ImageView; + + +public class ViewHolder { + public long id; + public String title; + public TextView text; + public ImageView arrow; +} From 1fd0a60fe1f980fcdeeff7e94d45b356a5db9b2b Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Wed, 5 Aug 2015 22:27:01 +0200 Subject: [PATCH 03/41] improve bitmap quality --- src/ch/blinkenlights/android/vanilla/CoverBitmap.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch/blinkenlights/android/vanilla/CoverBitmap.java b/src/ch/blinkenlights/android/vanilla/CoverBitmap.java index 1cbab620..64544ff2 100644 --- a/src/ch/blinkenlights/android/vanilla/CoverBitmap.java +++ b/src/ch/blinkenlights/android/vanilla/CoverBitmap.java @@ -322,7 +322,7 @@ public final class CoverBitmap { float scale = Math.min((float)width / sourceWidth, (float)height / sourceHeight); sourceWidth *= scale; sourceHeight *= scale; - return Bitmap.createScaledBitmap(source, sourceWidth, sourceHeight, false); + return Bitmap.createScaledBitmap(source, sourceWidth, sourceHeight, true); } /** From a4ad16832ce01116795572e047990e7b93586ccb Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Thu, 6 Aug 2015 11:19:15 +0200 Subject: [PATCH 04/41] Implement getSongByTypeId --- .../android/vanilla/MediaUtils.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/ch/blinkenlights/android/vanilla/MediaUtils.java b/src/ch/blinkenlights/android/vanilla/MediaUtils.java index ff963979..0c6b51af 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaUtils.java +++ b/src/ch/blinkenlights/android/vanilla/MediaUtils.java @@ -405,6 +405,27 @@ public class MediaUtils { sAllSongs = null; } + /** + * Returns the first matching song (or NULL) of given type + id combination + * + * @param resolver A ContentResolver to use. + * @param type The MediaTye to query + * @param id The id of given type to query + */ + public static Song getSongByTypeId(ContentResolver resolver, int type, long id) { + Song song = new Song(-1); + QueryTask query = buildQuery(type, id, Song.FILLED_PROJECTION, null); + Cursor cursor = query.runQuery(resolver); + if (cursor != null) { + if (cursor.getCount() > 0) { + cursor.moveToPosition(0); + song.populate(cursor); + } + cursor.close(); + } + return song.id == -1 ? null : song; + } + /** * Returns a song randomly selected from all the songs in the Android * MediaStore. From 8393184d496ce35eef3f8caf6f40bdb33fde6937 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Thu, 6 Aug 2015 11:19:48 +0200 Subject: [PATCH 05/41] The tag header shall use a normal ViewHolder --- src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index cf2e2af7..39704150 100644 --- a/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -350,7 +350,7 @@ public class LibraryPagerAdapter TextView headerText = (TextView)header.findViewById(R.id.text); headerText.setText(mHeaderText); headerText.setOnClickListener(this); - header.setTag(type); + header.setTag(new ViewHolder()); // behave like a normal library row view.addHeaderView(header); } view.setAdapter(adapter); @@ -822,6 +822,7 @@ public class LibraryPagerAdapter */ private static Intent createHeaderIntent(View header) { + header = (View)header.getParent(); // tag is set on parent view of header int type = (Integer)header.getTag(); Intent intent = new Intent(); intent.putExtra(LibraryAdapter.DATA_ID, LibraryAdapter.HEADER_ID); From 93b750dacdc84a64b2f43746eb9229cd432c389b Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 12:25:40 +0200 Subject: [PATCH 06/41] implement LazyCoverView --- res/layout/library_row_expandable.xml | 9 + .../android/vanilla/LazyCoverView.java | 216 ++++++++++++++++++ .../android/vanilla/LibraryPagerAdapter.java | 11 +- .../android/vanilla/MediaAdapter.java | 19 +- .../android/vanilla/ViewHolder.java | 1 + 5 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 src/ch/blinkenlights/android/vanilla/LazyCoverView.java 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; } From ca3202aed0f9fcd0d52dcbfaae70410241e17610 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 12:48:22 +0200 Subject: [PATCH 07/41] Cache only by albumId --- src/ch/blinkenlights/android/vanilla/Song.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ch/blinkenlights/android/vanilla/Song.java b/src/ch/blinkenlights/android/vanilla/Song.java index 2cedfa2a..f707004b 100644 --- a/src/ch/blinkenlights/android/vanilla/Song.java +++ b/src/ch/blinkenlights/android/vanilla/Song.java @@ -106,20 +106,18 @@ public class Song implements Comparable { private class LruCacheKey { long id; - long artistId; long albumId; String path; - public LruCacheKey(long id, long artistId, long albumId, String path) { + public LruCacheKey(long id, long albumId, String path) { this.id = id; - this.artistId = artistId; this.albumId = albumId; this.path = path; } @Override public boolean equals(Object obj) { - if (obj instanceof LruCacheKey && this.albumId == ((LruCacheKey)obj).albumId && this.artistId == ((LruCacheKey)obj).artistId) { + if (obj instanceof LruCacheKey && this.albumId == ((LruCacheKey)obj).albumId) { return true; } return false; @@ -127,7 +125,7 @@ public class Song implements Comparable { @Override public int hashCode() { - return (int)( 0xFFFFFF & (this.artistId + this.albumId) ); + return (int)( 0xFFFFFF & (this.albumId) ); } @Override @@ -378,7 +376,7 @@ public class Song implements Comparable { sCoverCache.evictAll(); } - LruCacheKey key = new LruCacheKey(id, artistId, albumId, path); + LruCacheKey key = new LruCacheKey(id, albumId, path); Bitmap cover = sCoverCache.get(key); if (cover == null) From 9bc275d199d1388e34ff471a4a54ddcb297b4a37 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 12:55:49 +0200 Subject: [PATCH 08/41] Cache covers by artist id --- .../android/vanilla/MediaAdapter.java | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java index 37a2093b..090fb18c 100644 --- a/src/ch/blinkenlights/android/vanilla/MediaAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/MediaAdapter.java @@ -137,9 +137,10 @@ public class MediaAdapter */ private boolean mExpandable; /** - * If true, return views with covers and fire callbacks + * Defines the media type to use for this entry + * Setting this to MediaUtils.TYPE_INVALID disables cover artwork */ - private boolean mHasCoverArt; + private int mCoverCacheType; /** * Construct a MediaAdapter representing the given type of @@ -159,6 +160,9 @@ public class MediaAdapter mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mLooper = looper; + mCoverCacheType = MediaUtils.TYPE_INVALID; + String coverCacheKey = "0"; // SQL dummy entry + switch (type) { case MediaUtils.TYPE_ARTIST: mStore = MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI; @@ -176,7 +180,8 @@ 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; + mCoverCacheType = MediaUtils.TYPE_ALBUM; + coverCacheKey = BaseColumns._ID; break; case MediaUtils.TYPE_SONG: mStore = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; @@ -186,7 +191,8 @@ 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; + mCoverCacheType = MediaUtils.TYPE_ALBUM; + coverCacheKey = MediaStore.Audio.Albums.ALBUM_ID; break; case MediaUtils.TYPE_PLAYLIST: mStore = MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI; @@ -208,9 +214,9 @@ public class MediaAdapter } if (mFields.length == 1) - mProjection = new String[] { BaseColumns._ID, mFields[0] }; + mProjection = new String[] { BaseColumns._ID, coverCacheKey, mFields[0] }; else - mProjection = new String[] { BaseColumns._ID, mFields[mFields.length - 1], mFields[0] }; + mProjection = new String[] { BaseColumns._ID, coverCacheKey, mFields[mFields.length - 1], mFields[0] }; } /** @@ -405,15 +411,15 @@ public class MediaAdapter switch (mType) { case MediaUtils.TYPE_ARTIST: - fields = new String[] { cursor.getString(1) }; + fields = new String[] { cursor.getString(2) }; data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, id); break; case MediaUtils.TYPE_ALBUM: - fields = new String[] { cursor.getString(2), cursor.getString(1) }; + fields = new String[] { cursor.getString(3), cursor.getString(2) }; data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, id); break; case MediaUtils.TYPE_GENRE: - fields = new String[] { cursor.getString(1) }; + fields = new String[] { cursor.getString(2) }; data = id; break; default: @@ -463,7 +469,7 @@ public class MediaAdapter holder.cover.setOnClickListener(this); holder.arrow.setVisibility(mExpandable ? View.VISIBLE : View.GONE); - holder.cover.setVisibility(mHasCoverArt ? View.VISIBLE : View.GONE); + holder.cover.setVisibility(mCoverCacheType != MediaUtils.TYPE_INVALID ? View.VISIBLE : View.GONE); holder.cover.setup(mLooper); } else { holder = (ViewHolder)view.getTag(); @@ -472,9 +478,10 @@ public class MediaAdapter Cursor cursor = mCursor; cursor.moveToPosition(position); holder.id = cursor.getLong(0); - if (mFields.length > 1) { - String line1 = cursor.getString(1); - String line2 = cursor.getString(2); + long cacheId = cursor.getLong(1); + if (mFields.length > 2) { + String line1 = cursor.getString(2); + String line2 = cursor.getString(3); if(line1 == null) { line1 = "???"; } if(line2 == null) { line2 = "???"; } SpannableStringBuilder sb = new SpannableStringBuilder(line1); @@ -484,14 +491,14 @@ public class MediaAdapter holder.text.setText(sb); holder.title = line1; } else { - String title = cursor.getString(1); + String title = cursor.getString(2); if(title == null) { title = "???"; } holder.text.setText(title); holder.title = title; } - if (mHasCoverArt) { - holder.cover.setCover(mType, holder.id); + if (mCoverCacheType != MediaUtils.TYPE_INVALID) { + holder.cover.setCover(mCoverCacheType, cacheId); } return view; From d200d45f2dfa238b0fb0ee295cfcafe0bfc33f38 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 13:13:38 +0200 Subject: [PATCH 09/41] change visibility of divider to avoid layout issues --- res/layout/library_row_expandable.xml | 1 + src/ch/blinkenlights/android/vanilla/FileSystemAdapter.java | 2 ++ src/ch/blinkenlights/android/vanilla/MediaAdapter.java | 2 ++ src/ch/blinkenlights/android/vanilla/ViewHolder.java | 2 ++ 4 files changed, 7 insertions(+) diff --git a/res/layout/library_row_expandable.xml b/res/layout/library_row_expandable.xml index 6277b69d..0a470b8b 100644 --- a/res/layout/library_row_expandable.xml +++ b/res/layout/library_row_expandable.xml @@ -47,6 +47,7 @@ THE SOFTWARE. android:id="@+id/divider" android:layout_width="1dip" android:layout_height="fill_parent" + android:visibility="gone" android:background="?divider_color" /> Date: Sun, 9 Aug 2015 13:16:30 +0200 Subject: [PATCH 10/41] FilebrowserStartAdapter shall also use the generic view holder --- .../android/vanilla/FilebrowserStartAdapter.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/ch/blinkenlights/android/vanilla/FilebrowserStartAdapter.java b/src/ch/blinkenlights/android/vanilla/FilebrowserStartAdapter.java index 54cfd106..72a5bfb1 100644 --- a/src/ch/blinkenlights/android/vanilla/FilebrowserStartAdapter.java +++ b/src/ch/blinkenlights/android/vanilla/FilebrowserStartAdapter.java @@ -45,13 +45,6 @@ public class FilebrowserStartAdapter mInflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } - private static class ViewHolder { - public int id; - public TextView text; - public View divider; - public ImageView arrow; - } - @Override public View getView(int pos, View convertView, ViewGroup parent) { View view; @@ -82,7 +75,7 @@ public class FilebrowserStartAdapter @Override public void onClick(View view) { ViewHolder holder = (ViewHolder)((View)view.getParent()).getTag(); - mActivity.onDirectoryClicked(holder.id); + mActivity.onDirectoryClicked((int)holder.id); } } From ab4b3aebcc532eae9d006b53cd3072963032ec74 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 13:42:20 +0200 Subject: [PATCH 11/41] update handler if looper changed (on rotation) --- src/ch/blinkenlights/android/vanilla/LazyCoverView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ch/blinkenlights/android/vanilla/LazyCoverView.java b/src/ch/blinkenlights/android/vanilla/LazyCoverView.java index bdf4ad8d..d40d00a8 100644 --- a/src/ch/blinkenlights/android/vanilla/LazyCoverView.java +++ b/src/ch/blinkenlights/android/vanilla/LazyCoverView.java @@ -126,7 +126,7 @@ public class LazyCoverView extends ImageView if (sUiHandler == null) { sUiHandler = new Handler(this); } - if (sHandler == null) { + if (sHandler == null || sHandler.getLooper().equals(looper) == false) { sHandler = new Handler(looper, this); } // image dimension we are going to cache - we should probably calculate this From 4f210a3e159188aecbfbc591b46fe07f8c7ccd69 Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sun, 9 Aug 2015 19:18:09 +0200 Subject: [PATCH 12/41] unify draggable_row --- res/layout-v21/draggable_row.xml | 72 -------------------------------- res/layout/draggable_row.xml | 2 +- res/values-v21/theme.xml | 3 ++ res/values/theme.xml | 3 ++ 4 files changed, 7 insertions(+), 73 deletions(-) delete mode 100644 res/layout-v21/draggable_row.xml diff --git a/res/layout-v21/draggable_row.xml b/res/layout-v21/draggable_row.xml deleted file mode 100644 index 219e06e5..00000000 --- a/res/layout-v21/draggable_row.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/res/layout/draggable_row.xml b/res/layout/draggable_row.xml index e69e5d7f..1d54c7a0 100644 --- a/res/layout/draggable_row.xml +++ b/res/layout/draggable_row.xml @@ -33,7 +33,7 @@ along with this program. If not, see . android:layout_width="4dip" android:layout_height="44dip" android:visibility="invisible" - android:background="@android:color/holo_blue_dark" /> + android:background="@color/now_playing_marker" /> @color/vanillaAccent @color/material_grey_600 + + @color/vanillaAccent +