From 20517a8281088160848100f9645ebf68461fcd6a Mon Sep 17 00:00:00 2001 From: Christopher Eby Date: Mon, 20 Feb 2012 23:03:33 -0600 Subject: [PATCH] Split library ListView/adapter related code out into LibraryPagerAdapter --- src/org/kreed/vanilla/FileSystemAdapter.java | 7 +- src/org/kreed/vanilla/LibraryActivity.java | 560 +++------------ .../kreed/vanilla/LibraryPagerAdapter.java | 635 ++++++++++++++++++ 3 files changed, 743 insertions(+), 459 deletions(-) create mode 100644 src/org/kreed/vanilla/LibraryPagerAdapter.java diff --git a/src/org/kreed/vanilla/FileSystemAdapter.java b/src/org/kreed/vanilla/FileSystemAdapter.java index 38c30c2c..6d362664 100644 --- a/src/org/kreed/vanilla/FileSystemAdapter.java +++ b/src/org/kreed/vanilla/FileSystemAdapter.java @@ -191,7 +191,10 @@ public class FileSystemAdapter extends BaseAdapter implements LibraryAdapter { @Override public void setFilter(String filter) { - mFilter = SPACE_SPLIT.split(filter.toLowerCase()); + if (filter == null) + mFilter = null; + else + mFilter = SPACE_SPLIT.split(filter.toLowerCase()); } @Override @@ -247,7 +250,7 @@ public class FileSystemAdapter extends BaseAdapter implements LibraryAdapter { @Override public void onEvent(int event, String path) { - mActivity.postRequestRequery(FileSystemAdapter.this); + mActivity.mPagerAdapter.postRequestRequery(FileSystemAdapter.this); } } } \ No newline at end of file diff --git a/src/org/kreed/vanilla/LibraryActivity.java b/src/org/kreed/vanilla/LibraryActivity.java index d7ef63ae..e39d7ac0 100644 --- a/src/org/kreed/vanilla/LibraryActivity.java +++ b/src/org/kreed/vanilla/LibraryActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2011 Christopher Eby + * Copyright (C) 2012 Christopher Eby * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -28,7 +28,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; -import android.database.ContentObserver; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Color; @@ -36,10 +35,8 @@ import android.graphics.drawable.PaintDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.os.Message; import android.provider.MediaStore; -import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.text.Editable; import android.text.TextUtils; @@ -77,23 +74,50 @@ public class LibraryActivity , TextWatcher , DialogInterface.OnClickListener , DialogInterface.OnDismissListener - , ViewPager.OnPageChangeListener { + /** + * Action for row click: play the row. + */ public static final int ACTION_PLAY = 0; + /** + * Action for row click: enqueue the row. + */ public static final int ACTION_ENQUEUE = 1; + /** + * Action for row click: perform the last used action. + */ public static final int ACTION_LAST_USED = 2; + /** + * Action for row click: play all the songs in the adapter, starting with + * the current row. + */ public static final int ACTION_PLAY_ALL = 3; + /** + * Action for row click: enqueue all the songs in the adapter, starting with + * the current row. + */ public static final int ACTION_ENQUEUE_ALL = 4; + /** + * Action for row click: do nothing. + */ public static final int ACTION_DO_NOTHING = 5; + /** + * Action for row click: expand the row. + */ public static final int ACTION_EXPAND = 6; + /** + * The SongTimeline add song modes corresponding to each relevant action. + */ private static final int[] modeForAction = { SongTimeline.MODE_PLAY, SongTimeline.MODE_ENQUEUE, -1, SongTimeline.MODE_PLAY_ID_FIRST, SongTimeline.MODE_ENQUEUE_ID_FIRST }; public ViewPager mViewPager; + private View mSearchBox; private boolean mSearchBoxVisible; - TextView mTextFilter; + + private TextView mTextFilter; private View mClearButton; private View mActionControls; @@ -106,151 +130,27 @@ public class LibraryActivity private HorizontalScrollView mLimiterScroller; private ViewGroup mLimiterViews; + /** + * The action to execute when a row is tapped. + */ private int mDefaultAction; - + /** + * The last used action from the menu. Used with ACTION_LAST_USED. + */ private int mLastAction = ACTION_PLAY; + /** + * The id of the media that was last pressed in the current adapter. Used to + * open the playback activity when an item is pressed twice. + */ private long mLastActedId; - /** - * The number of adapters/lists (determines array sizes). + * The pager adapter that manages each media ListView. */ - private static final int ADAPTER_COUNT = 6; + public LibraryPagerAdapter mPagerAdapter; /** - * The ListView for each adapter, in the same order as MediaUtils.TYPE_*. + * The adapter for the currently visible list. */ - final ListView[] mLists = new ListView[ADAPTER_COUNT]; - /** - * Whether the adapter corresponding to each index has stale data. - */ - final boolean[] mRequeryNeeded = new boolean[ADAPTER_COUNT]; - /** - * Each adapter, in the same order as MediaUtils.TYPE_*. - */ - final LibraryAdapter[] mAdapters = new LibraryAdapter[ADAPTER_COUNT]; - /** - * The human-readable title for each page. - */ - public static final int[] TITLES = { R.string.artists, R.string.albums, R.string.songs, - R.string.playlists, R.string.genres, R.string.files }; - /** - * Limiters that should be passed to an adapter constructor. - */ - public final Limiter[] mPendingLimiters = new Limiter[ADAPTER_COUNT]; - MediaAdapter mArtistAdapter; - MediaAdapter mAlbumAdapter; - MediaAdapter mSongAdapter; - MediaAdapter mPlaylistAdapter; - MediaAdapter mGenreAdapter; - FileSystemAdapter mFilesAdapter; - LibraryAdapter mCurrentAdapter; - - private final ContentObserver mPlaylistObserver = new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) - { - if (mPlaylistAdapter != null) { - postRequestRequery(mPlaylistAdapter); - } - } - }; - - private final PagerAdapter mPagerAdapter = new PagerAdapter() { - @Override - public Object instantiateItem(ViewGroup container, int position) - { - ListView view = mLists[position]; - - if (view == null) { - LibraryAdapter adapter; - - switch (position) { - case 0: - adapter = mArtistAdapter = new MediaAdapter(LibraryActivity.this, MediaUtils.TYPE_ARTIST, true, true, null); - mArtistAdapter.setHeaderText(getHeaderText()); - break; - case 1: - adapter = mAlbumAdapter = new MediaAdapter(LibraryActivity.this, MediaUtils.TYPE_ALBUM, true, true, mPendingLimiters[position]); - mAlbumAdapter.setHeaderText(getHeaderText()); - break; - case 2: - adapter = mSongAdapter = new MediaAdapter(LibraryActivity.this, MediaUtils.TYPE_SONG, false, true, mPendingLimiters[position]); - mSongAdapter.setHeaderText(getHeaderText()); - break; - case 3: - adapter = mPlaylistAdapter = new MediaAdapter(LibraryActivity.this, MediaUtils.TYPE_PLAYLIST, true, false, null); - break; - case 4: - adapter = mGenreAdapter = new MediaAdapter(LibraryActivity.this, MediaUtils.TYPE_GENRE, true, false, null); - break; - case 5: - adapter = mFilesAdapter = new FileSystemAdapter(LibraryActivity.this, mPendingLimiters[position]); - break; - default: - throw new IllegalArgumentException("Invalid position: " + position); - } - - view = new ListView(LibraryActivity.this); - view.setOnItemClickListener(LibraryActivity.this); - view.setOnCreateContextMenuListener(LibraryActivity.this); - view.setDivider(null); - view.setFastScrollEnabled(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - CompatHoneycomb.setFastScrollAlwaysVisible(view, true); - } - view.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); - - view.setAdapter(adapter); - if (position != 5) - loadSortOrder((MediaAdapter)adapter); - - String filter = mTextFilter.getText().toString(); - if (filter.length() != 0) { - adapter.setFilter(filter); - } - - mAdapters[position] = adapter; - mLists[position] = view; - mRequeryNeeded[position] = true; - - if (mCurrentAdapter == null && mViewPager.getCurrentItem() == position) { - // Special case: for first time initialization onPageSelected - // is not called (for initial position = 0) or called before - // the adapter is initialized (for all other initial positions). - // So do required work here. - mCurrentAdapter = adapter; - updateLimiterViews(); - } - } - - requeryIfNeeded(position); - container.addView(view); - return view; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) - { - container.removeView(mLists[position]); - } - - @Override - public CharSequence getPageTitle(int position) - { - return getResources().getText(TITLES[position]); - } - - @Override - public int getCount() - { - return ADAPTER_COUNT; - } - - @Override - public boolean isViewFromObject(View view, Object object) - { - return view == object; - } - }; + private LibraryAdapter mCurrentAdapter; @Override public void onCreate(Bundle state) @@ -275,12 +175,16 @@ public class LibraryActivity mLimiterScroller = (HorizontalScrollView)findViewById(R.id.limiter_scroller); mLimiterViews = (ViewGroup)findViewById(R.id.limiter_layout); - mViewPager = (ViewPager)findViewById(R.id.pager); - mViewPager.setAdapter(mPagerAdapter); + LibraryPagerAdapter pagerAdapter = new LibraryPagerAdapter(this, mLooper); + mPagerAdapter = pagerAdapter; + + ViewPager pager = (ViewPager)findViewById(R.id.pager); + pager.setAdapter(pagerAdapter); + mViewPager = pager; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { CompatHoneycomb.addActionBarTabs(this); - mViewPager.setOnPageChangeListener(this); + pager.setOnPageChangeListener(pagerAdapter); View controls = getLayoutInflater().inflate(R.layout.actionbar_controls, null); mTitle = (TextView)controls.findViewById(R.id.title); @@ -290,8 +194,8 @@ public class LibraryActivity mActionControls = controls; } else { TabPageIndicator tabs = new TabPageIndicator(this); - tabs.setViewPager(mViewPager); - tabs.setOnPageChangeListener(this); + tabs.setViewPager(pager); + tabs.setOnPageChangeListener(pagerAdapter); LinearLayout content = (LinearLayout)findViewById(R.id.content); content.addView(tabs, 0, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); @@ -326,8 +230,6 @@ public class LibraryActivity } } - getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver); - loadAlbumIntent(getIntent()); } @@ -370,7 +272,12 @@ public class LibraryActivity String[] fields = { intent.getStringExtra("artist"), intent.getStringExtra("album") }; String data = String.format("album_id=%d", albumId); Limiter limiter = new Limiter(MediaUtils.TYPE_ALBUM, fields, data); - setLimiter(limiter, true); + int tab = mPagerAdapter.setLimiter(limiter); + if (tab == mViewPager.getCurrentItem()) + updateLimiterViews(); + else + mViewPager.setCurrentItem(tab); + } } @@ -387,26 +294,16 @@ public class LibraryActivity @Override public void onRestoreInstanceState(Bundle in) { - super.onRestoreInstanceState(in); if (in.getBoolean("search_box_visible")) setSearchBoxVisible(true); - mPendingLimiters[1] = (Limiter)in.getSerializable("limiter_albums"); - mPendingLimiters[2] = (Limiter)in.getSerializable("limiter_songs"); - mPendingLimiters[5] = (Limiter)in.getSerializable("limiter_files"); + super.onRestoreInstanceState(in); } @Override protected void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); - out.putBoolean("search_box_visible", mSearchBoxVisible); - if (mAlbumAdapter != null) - out.putSerializable("limiter_albums", mAlbumAdapter.getLimiter()); - if (mSongAdapter != null) - out.putSerializable("limiter_songs", mSongAdapter.getLimiter()); - if (mFilesAdapter != null) - out.putSerializable("limiter_files", mFilesAdapter.getLimiter()); } @Override @@ -448,31 +345,18 @@ public class LibraryActivity return false; } - /** - * Returns either "Play All" or "Enqueue All", depending on the current - * default action. - */ - public String getHeaderText() - { - int action = mDefaultAction; - if (action == ACTION_LAST_USED) - action = mLastAction; - return getString(action == ACTION_ENQUEUE || action == ACTION_ENQUEUE_ALL ? R.string.enqueue_all : R.string.play_all); - } - /** * Update the first row of the lists with the appropriate action (play all * or enqueue all). */ private void updateHeaders() { - String text = getHeaderText(); - if (mArtistAdapter != null) - mArtistAdapter.setHeaderText(text); - if (mAlbumAdapter != null) - mAlbumAdapter.setHeaderText(text); - if (mSongAdapter != null) - mSongAdapter.setHeaderText(text); + int action = mDefaultAction; + if (action == ACTION_LAST_USED) + action = mLastAction; + boolean isEnqueue = action == ACTION_ENQUEUE || action == ACTION_ENQUEUE_ALL; + String text = getString(isEnqueue ? R.string.enqueue_all : R.string.play_all); + mPagerAdapter.setHeaderText(text); } /** @@ -484,17 +368,13 @@ public class LibraryActivity */ private void pickSongs(Intent intent, int action) { - if (action == ACTION_LAST_USED) - action = mLastAction; - long id = intent.getLongExtra("id", -1); boolean all = false; int mode = action; if (action == ACTION_PLAY_ALL || action == ACTION_ENQUEUE_ALL) { - LibraryAdapter adapter = mCurrentAdapter; - boolean notPlayAllAdapter = adapter != mSongAdapter && adapter != mAlbumAdapter - && adapter != mArtistAdapter || id == MediaView.HEADER_ID; + int type = mCurrentAdapter.getMediaType(); + boolean notPlayAllAdapter = type > MediaUtils.TYPE_SONG || id == MediaView.HEADER_ID; if (mode == ACTION_ENQUEUE_ALL && notPlayAllAdapter) { mode = ACTION_ENQUEUE; } else if (mode == ACTION_PLAY_ALL && notPlayAllAdapter) { @@ -527,115 +407,11 @@ public class LibraryActivity { int type = intent.getIntExtra("type", 1); long id = intent.getLongExtra("id", -1); - setLimiter(mAdapters[type - 1].buildLimiter(id), true); - } - - /** - * Clear a limiter. - * - * @param type Which type of limiter to clear. - */ - private void clearLimiter(int type) - { - if (type == MediaUtils.TYPE_FILE) { - if (mFilesAdapter == null) { - mPendingLimiters[5] = null; - } else { - mFilesAdapter.setLimiter(null); - requestRequery(mFilesAdapter); - } - } else { - if (mAlbumAdapter == null) { - mPendingLimiters[1] = null; - } else { - mAlbumAdapter.setLimiter(null); - loadSortOrder(mAlbumAdapter); - requestRequery(mAlbumAdapter); - } - if (mSongAdapter == null) { - mPendingLimiters[2] = null; - } else { - mSongAdapter.setLimiter(null); - loadSortOrder(mSongAdapter); - requestRequery(mSongAdapter); - } - } - updateLimiterViews(); - } - - /** - * Update the adapters with the given limiter. - * - * @param switchTab If true, will switch to the tab appropriate for - * expanding a row. - */ - private void setLimiter(Limiter limiter, boolean switchTab) - { - int tab; - - switch (limiter.type) { - case MediaUtils.TYPE_ALBUM: - if (mSongAdapter == null) { - mPendingLimiters[2] = limiter; - } else { - mSongAdapter.setLimiter(limiter); - loadSortOrder(mSongAdapter); - requestRequery(mSongAdapter); - } - tab = 2; - break; - case MediaUtils.TYPE_ARTIST: - if (mAlbumAdapter == null) { - mPendingLimiters[1] = limiter; - } else { - mAlbumAdapter.setLimiter(limiter); - loadSortOrder(mAlbumAdapter); - requestRequery(mAlbumAdapter); - } - if (mSongAdapter == null) { - mPendingLimiters[2] = limiter; - } else { - mSongAdapter.setLimiter(limiter); - loadSortOrder(mSongAdapter); - requestRequery(mSongAdapter); - } - tab = 1; - break; - case MediaUtils.TYPE_GENRE: - if (mAlbumAdapter == null) { - mPendingLimiters[1] = limiter; - } else { - mAlbumAdapter.setLimiter(limiter); - loadSortOrder(mAlbumAdapter); - requestRequery(mAlbumAdapter); - } - if (mSongAdapter == null) { - mPendingLimiters[2] = null; - } else { - mSongAdapter.setLimiter(limiter); - loadSortOrder(mSongAdapter); - requestRequery(mSongAdapter); - } - tab = 2; - break; - case MediaUtils.TYPE_FILE: - if (mFilesAdapter == null) { - mPendingLimiters[5] = limiter; - } else { - mFilesAdapter.setLimiter(limiter); - requestRequery(mFilesAdapter); - } - tab = 5; - break; - default: - throw new IllegalArgumentException("Unsupported limiter type: " + limiter.type); - } - - if (switchTab && mViewPager.getCurrentItem() != tab) { - mViewPager.setCurrentItem(tab); - } else { + int tab = mPagerAdapter.setLimiter(mPagerAdapter.mAdapters[type - 1].buildLimiter(id)); + if (tab == mViewPager.getCurrentItem()) updateLimiterViews(); - } + else + mViewPager.setCurrentItem(tab); } /** @@ -653,8 +429,10 @@ public class LibraryActivity MediaView mediaView = (MediaView)view; LibraryAdapter adapter = (LibraryAdapter)list.getAdapter(); int action = mDefaultAction; + if (action == ACTION_LAST_USED) + action = mLastAction; if (mediaView.isRightBitmapPressed() || action == ACTION_EXPAND && mediaView.hasRightBitmap()) { - if (adapter == mPlaylistAdapter) + if (adapter.getMediaType() == MediaUtils.TYPE_PLAYLIST) editPlaylist(mediaView.getMediaId(), mediaView.getTitle()); else expand(createClickIntent(adapter, mediaView)); @@ -683,13 +461,7 @@ public class LibraryActivity @Override public void onTextChanged(CharSequence text, int start, int before, int count) { - String filter = text.toString(); - for (LibraryAdapter adapter : mAdapters) { - if (adapter != null) { - adapter.setFilter(filter); - requestRequery(adapter); - } - } + mPagerAdapter.setFilter(text.toString()); } /** @@ -699,9 +471,8 @@ public class LibraryActivity { mLimiterViews.removeAllViews(); - LibraryAdapter adapter = mCurrentAdapter; - Limiter limiterData; - if (adapter != null && (limiterData = adapter.getLimiter()) != null) { + Limiter limiterData = mPagerAdapter.getCurrentLimiter(); + if (limiterData != null) { String[] limiter = limiterData.names; LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -745,7 +516,7 @@ public class LibraryActivity // a limiter view was clicked int i = (Integer)view.getTag(); - Limiter limiter = mCurrentAdapter.getLimiter(); + Limiter limiter = mPagerAdapter.getCurrentLimiter(); int type = limiter.type; if (i == 1 && type == MediaUtils.TYPE_ALBUM) { ContentResolver resolver = getContentResolver(); @@ -756,7 +527,8 @@ public class LibraryActivity if (cursor.moveToNext()) { String[] fields = { limiter.names[0] }; String data = String.format("artist_id=%d", cursor.getLong(0)); - setLimiter(new Limiter(MediaUtils.TYPE_ARTIST, fields, data), false); + mPagerAdapter.setLimiter(new Limiter(MediaUtils.TYPE_ARTIST, fields, data)); + updateLimiterViews(); } cursor.close(); } @@ -767,9 +539,11 @@ public class LibraryActivity while (--diff != -1) { file = file.getParentFile(); } - setLimiter(FileSystemAdapter.buildLimiter(file), false); + mPagerAdapter.setLimiter(FileSystemAdapter.buildLimiter(file)); + updateLimiterViews(); } else { - clearLimiter(type); + mPagerAdapter.clearLimiter(type); + updateLimiterViews(); } } else { super.onClick(view); @@ -828,7 +602,7 @@ public class LibraryActivity if (type == MediaUtils.TYPE_FILE) { query = MediaUtils.buildFileQuery(intent.getStringExtra("file"), projection); } else if (all || id == MediaView.HEADER_ID) { - query = ((MediaAdapter)mAdapters[type - 1]).buildSongQuery(projection); + query = ((MediaAdapter)mPagerAdapter.mAdapters[type - 1]).buildSongQuery(projection); query.setExtra(id); } else { query = MediaUtils.buildQuery(type, id, projection, null); @@ -865,7 +639,8 @@ public class LibraryActivity Intent intent = createClickIntent(adapter, view); boolean isHeader = view.getMediaId() == MediaView.HEADER_ID; - boolean isAllAdapter = adapter == mArtistAdapter || adapter == mAlbumAdapter || adapter == mSongAdapter; + int type = adapter.getMediaType(); + boolean isAllAdapter = type <= MediaUtils.TYPE_SONG; if (isHeader) menu.setHeaderTitle(getString(R.string.all_songs)); @@ -878,7 +653,7 @@ public class LibraryActivity menu.add(0, MENU_ENQUEUE, 0, R.string.enqueue).setIntent(intent); if (isAllAdapter) menu.add(0, MENU_ENQUEUE_ALL, 0, R.string.enqueue_all).setIntent(intent); - if (adapter == mPlaylistAdapter) { + if (type == MediaUtils.TYPE_PLAYLIST) { menu.add(0, MENU_RENAME_PLAYLIST, 0, R.string.rename).setIntent(intent); menu.add(0, MENU_EDIT, 0, R.string.edit).setIntent(intent); } @@ -962,6 +737,10 @@ public class LibraryActivity switch (item.getItemId()) { case MENU_EXPAND: expand(intent); + if (mDefaultAction == ACTION_LAST_USED && mLastAction != ACTION_EXPAND) { + mLastAction = ACTION_EXPAND; + updateHeaders(); + } break; case MENU_ENQUEUE: pickSongs(intent, ACTION_ENQUEUE); @@ -1039,7 +818,8 @@ public class LibraryActivity @Override public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(MENU_SORT).setEnabled(mCurrentAdapter != mFilesAdapter); + LibraryAdapter adapter = mCurrentAdapter; + menu.findItem(MENU_SORT).setEnabled(adapter != null && adapter.getMediaType() != MediaUtils.TYPE_FILE); return super.onPrepareOptionsMenu(menu); } @@ -1104,24 +884,10 @@ public class LibraryActivity * obj. */ private static final int MSG_RENAME_PLAYLIST = 13; - /** - * Called by MediaAdapters to requery their data on the worker thread. - * obj will contain the MediaAdapter. - */ - private static final int MSG_RUN_QUERY = 14; /** * Call addToPlaylist with data from the intent in obj. */ private static final int MSG_ADD_TO_PLAYLIST = 15; - /** - * Save the sort mode for the adapter passed in obj. - */ - private static final int MSG_SAVE_SORT = 16; - /** - * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the adapter - * passed in obj. - */ - private static final int MSG_REQUEST_REQUERY = 17; @Override public boolean handleMessage(Message message) @@ -1154,31 +920,6 @@ public class LibraryActivity } break; } - case MSG_RUN_QUERY: { - final LibraryAdapter adapter = (LibraryAdapter)message.obj; - final Object data = adapter.query(); - runOnUiThread(new Runnable() { - @Override - public void run() - { - adapter.commitQuery(data); - // scroll to the top of the list - mLists[adapter.getMediaType() - 1].setSelection(0); - } - }); - mRequeryNeeded[adapter.getMediaType() - 1] = false; - break; - } - case MSG_SAVE_SORT: { - MediaAdapter adapter = (MediaAdapter)message.obj; - SharedPreferences.Editor editor = PlaybackService.getSettings(this).edit(); - editor.putInt(String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()), adapter.getSortMode()); - editor.commit(); - break; - } - case MSG_REQUEST_REQUERY: - requestRequery((LibraryAdapter)message.obj); - break; default: return super.handleMessage(message); } @@ -1186,56 +927,10 @@ public class LibraryActivity return true; } - /** - * Requery the given adapter. If it is the current adapter, requery - * immediately. Otherwise, mark the adapter as needing a requery and requery - * when its tab is selected. - * - * Must be called on the UI thread. - */ - public void requestRequery(LibraryAdapter adapter) - { - if (adapter == mCurrentAdapter) { - runQuery(adapter); - } else { - mRequeryNeeded[adapter.getMediaType() - 1] = true; - // Clear the data for non-visible adapters (so we don't show the old - // data briefly when we later switch to that adapter) - adapter.clear(); - } - } - - /** - * Schedule a query to be run for the given adapter on the worker thread. - * - * @param adapter The adapter to run the query for. - */ - private void runQuery(LibraryAdapter adapter) - { - mHandler.removeMessages(MSG_RUN_QUERY, adapter); - mHandler.sendMessage(mHandler.obtainMessage(MSG_RUN_QUERY, adapter)); - } - @Override public void onMediaChange() { - for (LibraryAdapter adapter : mAdapters) { - if (adapter != null) { - postRequestRequery(adapter); - } - } - } - - /** - * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the UI - * thread. - * - * @param adapter The adapter, passed to requestRequery. - */ - public void postRequestRequery(LibraryAdapter adapter) - { - Handler handler = mUiHandler; - handler.sendMessage(handler.obtainMessage(MSG_REQUEST_REQUERY, adapter)); + mPagerAdapter.invalidateData(); } private void setSearchBoxVisible(boolean visible) @@ -1313,20 +1008,6 @@ public class LibraryActivity } } - /** - * Set the saved sort mode for the given adapter. The adapter should - * be re-queried after calling this. - * - * @param adapter The adapter to load for. - */ - public void loadSortOrder(MediaAdapter adapter) - { - String key = String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()); - int def = adapter.getDefaultSortMode(); - int sort = PlaybackService.getSettings(this).getInt(key, def); - adapter.setSortMode(sort); - } - @Override public void onClick(DialogInterface dialog, int which) { @@ -1336,8 +1017,6 @@ public class LibraryActivity @Override public void onDismiss(DialogInterface dialog) { - MediaAdapter adapter = (MediaAdapter)mCurrentAdapter; - ListView list = ((AlertDialog)dialog).getListView(); // subtract 1 for header int which = list.getCheckedItemPosition() - 1; @@ -1346,54 +1025,21 @@ public class LibraryActivity if (group.getCheckedRadioButtonId() == R.id.descending) which = ~which; - adapter.setSortMode(which); - requestRequery(adapter); - - // Force a new FastScroller to be created so the scroll sections - // are updated. - ListView view = mLists[mViewPager.getCurrentItem()]; - view.setFastScrollEnabled(false); - view.setFastScrollEnabled(true); - - mHandler.sendMessage(mHandler.obtainMessage(MSG_SAVE_SORT, adapter)); + mPagerAdapter.setSortMode(which); } /** - * Requery the adapter at the given position if it exists and needs a requery. + * Called when a new adapter has been made visible. * - * @param position An index in mAdapters. + * @param adapter The new visible adapter. */ - public void requeryIfNeeded(int position) + public void onAdapterSelected(LibraryAdapter adapter) { - LibraryAdapter adapter = mAdapters[position]; - if (adapter != null && mRequeryNeeded[position]) { - runQuery(adapter); - } - } - - @Override - public void onPageScrollStateChanged(int arg0) - { - } - - @Override - public void onPageScrolled(int arg0, float arg1, int arg2) - { - } - - @Override - public void onPageSelected(int position) - { - mCurrentAdapter = mAdapters[position]; - requeryIfNeeded(position); - if (position < ADAPTER_COUNT - 1) - requeryIfNeeded(position + 1); - if (position > 0) - requeryIfNeeded(position - 1); + mCurrentAdapter = adapter; + mLastActedId = -2; updateLimiterViews(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - CompatHoneycomb.selectTab(this, position); + CompatHoneycomb.selectTab(this, mViewPager.getCurrentItem()); } } } diff --git a/src/org/kreed/vanilla/LibraryPagerAdapter.java b/src/org/kreed/vanilla/LibraryPagerAdapter.java new file mode 100644 index 00000000..bfabbc46 --- /dev/null +++ b/src/org/kreed/vanilla/LibraryPagerAdapter.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2012 Christopher Eby + * + * 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 android.content.SharedPreferences; +import android.database.ContentObserver; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Parcelable; +import android.provider.MediaStore; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +/** + * PagerAdapter that manages the library media ListViews. + */ +public class LibraryPagerAdapter extends PagerAdapter implements Handler.Callback, ViewPager.OnPageChangeListener { + /** + * The number of adapters/lists (determines array sizes). + */ + private static final int ADAPTER_COUNT = 6; + /** + * The human-readable title for each page. + */ + private static final int[] TITLES = { R.string.artists, R.string.albums, R.string.songs, + R.string.playlists, R.string.genres, R.string.files }; + + /** + * The ListView for each adapter, in the same order as MediaUtils.TYPE_*. + */ + private final ListView[] mLists = new ListView[ADAPTER_COUNT]; + /** + * Whether the adapter corresponding to each index has stale data. + */ + private final boolean[] mRequeryNeeded = new boolean[ADAPTER_COUNT]; + /** + * Each adapter, in the same order as MediaUtils.TYPE_*. + */ + public final LibraryAdapter[] mAdapters = new LibraryAdapter[ADAPTER_COUNT]; + /** + * The artist adapter instance, also stored at mAdapters[0]. + */ + private MediaAdapter mArtistAdapter; + /** + * The album adapter instance, also stored at mAdapters[1]. + */ + private MediaAdapter mAlbumAdapter; + /** + * The song adapter instance, also stored at mAdapters[2]. + */ + private MediaAdapter mSongAdapter; + /** + * The playlist adapter instance, also stored at mAdapters[3]. + */ + private MediaAdapter mPlaylistAdapter; + /** + * The file adapter instance, also stored at mAdapters[5]. + */ + private FileSystemAdapter mFilesAdapter; + /** + * The adapter of the currently visible list. + */ + private LibraryAdapter mCurrentAdapter; + /** + * The index of the current page. + */ + private int mCurrentPage; + /** + * A limiter that should be set when the album adapter is created. + */ + private Limiter mPendingAlbumLimiter; + /** + * A limiter that should be set when the song adapter is created. + */ + private Limiter mPendingSongLimiter; + /** + * A limiter that should be set when the files adapter is created. + */ + private Limiter mPendingFileLimiter; + /** + * List positions stored in the saved state, or null if none were stored. + */ + private int[] mSavedPositions; + /** + * The LibraryActivity that owns this adapter. The adapter will be notified + * of changes in the current page. + */ + private final LibraryActivity mActivity; + /** + * A Handler running on the UI thread. + */ + private final Handler mUiHandler; + /** + * A Handler runing on a worker thread. + */ + private final Handler mWorkerHandler; + /** + * The text to be displayed in the first row of the artist, album, and + * song limiters. + */ + private String mHeaderText; + /** + * The current filter text, or null if none. + */ + private String mFilter; + + private final ContentObserver mPlaylistObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) + { + if (mPlaylistAdapter != null) { + postRequestRequery(mPlaylistAdapter); + } + } + }; + + /** + * Create the LibraryPager. + * + * @param activity The LibraryActivity that will own this adapter. The activity + * will receive callbacks from the ListViews. + * @param workerLooper A Looper running on a worker thread. + */ + public LibraryPagerAdapter(LibraryActivity activity, Looper workerLooper) + { + mActivity = activity; + mUiHandler = new Handler(this); + mWorkerHandler = new Handler(workerLooper, this); + mCurrentPage = -1; + activity.getContentResolver().registerContentObserver(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, true, mPlaylistObserver); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) + { + ListView view = mLists[position]; + + if (view == null) { + LibraryActivity activity = mActivity; + LibraryAdapter adapter; + + switch (position) { + case 0: + adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, true, true, null); + mArtistAdapter.setHeaderText(mHeaderText); + break; + case 1: + adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, true, true, mPendingAlbumLimiter); + mPendingAlbumLimiter = null; + mAlbumAdapter.setHeaderText(mHeaderText); + break; + case 2: + adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, false, true, mPendingSongLimiter); + mPendingSongLimiter = null; + mSongAdapter.setHeaderText(mHeaderText); + break; + case 3: + adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, true, false, null); + break; + case 4: + adapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, true, false, null); + break; + case 5: + adapter = mFilesAdapter = new FileSystemAdapter(activity, mPendingFileLimiter); + mPendingFileLimiter = null; + break; + default: + throw new IllegalArgumentException("Invalid position: " + position); + } + + view = new ListView(activity); + view.setOnItemClickListener(activity); + view.setOnCreateContextMenuListener(activity); + view.setDivider(null); + view.setFastScrollEnabled(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + CompatHoneycomb.setFastScrollAlwaysVisible(view, true); + } + view.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_INSET); + + view.setAdapter(adapter); + if (position != 5) + loadSortOrder((MediaAdapter)adapter); + + adapter.setFilter(mFilter); + + mAdapters[position] = adapter; + mLists[position] = view; + mRequeryNeeded[position] = true; + } + + requeryIfNeeded(position); + container.addView(view); + return view; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) + { + container.removeView(mLists[position]); + } + + @Override + public CharSequence getPageTitle(int position) + { + return mActivity.getResources().getText(TITLES[position]); + } + + @Override + public int getCount() + { + return ADAPTER_COUNT; + } + + @Override + public boolean isViewFromObject(View view, Object object) + { + return view == object; + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) + { + LibraryAdapter adapter = mAdapters[position]; + if (adapter != mCurrentAdapter) { + requeryIfNeeded(position); + mCurrentAdapter = adapter; + mCurrentPage = position; + mActivity.onAdapterSelected(adapter); + } + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) + { + Bundle in = (Bundle)state; + mPendingAlbumLimiter = (Limiter)in.getSerializable("limiter_albums"); + mPendingSongLimiter = (Limiter)in.getSerializable("limiter_songs"); + mPendingFileLimiter = (Limiter)in.getSerializable("limiter_files"); + mSavedPositions = in.getIntArray("pos"); + } + + @Override + public Parcelable saveState() + { + Bundle out = new Bundle(10); + if (mAlbumAdapter != null) + out.putSerializable("limiter_albums", mAlbumAdapter.getLimiter()); + if (mSongAdapter != null) + out.putSerializable("limiter_songs", mSongAdapter.getLimiter()); + if (mFilesAdapter != null) + out.putSerializable("limiter_files", mFilesAdapter.getLimiter()); + + int[] savedPositions = new int[ADAPTER_COUNT]; + ListView[] lists = mLists; + for (int i = ADAPTER_COUNT; --i != -1; ) { + if (lists[i] != null) { + savedPositions[i] = lists[i].getFirstVisiblePosition(); + } + } + out.putIntArray("pos", savedPositions); + return out; + } + + /** + * Sets the text to be displayed in the first row of the artist, album, and + * song lists. + */ + public void setHeaderText(String text) + { + if (mArtistAdapter != null) + mArtistAdapter.setHeaderText(text); + if (mAlbumAdapter != null) + mAlbumAdapter.setHeaderText(text); + if (mSongAdapter != null) + mSongAdapter.setHeaderText(text); + mHeaderText = text; + } + + /** + * Clear a limiter. + * + * @param type Which type of limiter to clear. + */ + public void clearLimiter(int type) + { + if (type == MediaUtils.TYPE_FILE) { + if (mFilesAdapter == null) { + mPendingFileLimiter = null; + } else { + mFilesAdapter.setLimiter(null); + requestRequery(mFilesAdapter); + } + } else { + if (mAlbumAdapter == null) { + mPendingAlbumLimiter = null; + } else { + mAlbumAdapter.setLimiter(null); + loadSortOrder(mAlbumAdapter); + requestRequery(mAlbumAdapter); + } + if (mSongAdapter == null) { + mPendingSongLimiter = null; + } else { + mSongAdapter.setLimiter(null); + loadSortOrder(mSongAdapter); + requestRequery(mSongAdapter); + } + } + } + + /** + * Update the adapters with the given limiter. + * + * @param limiter The limiter to set. + * @return The tab appropriate for expanding a row. + */ + public int setLimiter(Limiter limiter) + { + int tab; + + switch (limiter.type) { + case MediaUtils.TYPE_ALBUM: + if (mSongAdapter == null) { + mPendingSongLimiter = limiter; + } else { + mSongAdapter.setLimiter(limiter); + loadSortOrder(mSongAdapter); + requestRequery(mSongAdapter); + } + tab = 2; + break; + case MediaUtils.TYPE_ARTIST: + if (mAlbumAdapter == null) { + mPendingAlbumLimiter = limiter; + } else { + mAlbumAdapter.setLimiter(limiter); + loadSortOrder(mAlbumAdapter); + requestRequery(mAlbumAdapter); + } + if (mSongAdapter == null) { + mPendingSongLimiter = limiter; + } else { + mSongAdapter.setLimiter(limiter); + loadSortOrder(mSongAdapter); + requestRequery(mSongAdapter); + } + tab = 1; + break; + case MediaUtils.TYPE_GENRE: + if (mAlbumAdapter == null) { + mPendingAlbumLimiter = limiter; + } else { + mAlbumAdapter.setLimiter(limiter); + loadSortOrder(mAlbumAdapter); + requestRequery(mAlbumAdapter); + } + if (mSongAdapter == null) { + mPendingSongLimiter = null; + } else { + mSongAdapter.setLimiter(limiter); + loadSortOrder(mSongAdapter); + requestRequery(mSongAdapter); + } + tab = 2; + break; + case MediaUtils.TYPE_FILE: + if (mFilesAdapter == null) { + mPendingFileLimiter = limiter; + } else { + mFilesAdapter.setLimiter(limiter); + requestRequery(mFilesAdapter); + } + tab = 5; + break; + default: + throw new IllegalArgumentException("Unsupported limiter type: " + limiter.type); + } + + return tab; + } + + /** + * Returns the limiter set on the current adapter or null if there is none. + */ + public Limiter getCurrentLimiter() + { + LibraryAdapter current = mCurrentAdapter; + if (current == null) + return null; + return current.getLimiter(); + } + + /** + * Run on query on the adapter passed in obj. + * + * Runs on worker thread. + */ + private static final int MSG_RUN_QUERY = 0; + /** + * Save the sort mode for the adapter passed in obj. + * + * Runs on worker thread. + */ + private static final int MSG_SAVE_SORT = 1; + /** + * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the adapter + * passed in obj. + * + * Runs on worker thread. + */ + private static final int MSG_REQUEST_REQUERY = 2; + /** + * Commit the cursor passed in obj to the adapter at the index passed in + * arg1. + * + * Runs on UI thread. + */ + private static final int MSG_COMMIT_QUERY = 3; + + @Override + public boolean handleMessage(Message message) + { + switch (message.what) { + case MSG_RUN_QUERY: { + LibraryAdapter adapter = (LibraryAdapter)message.obj; + int index = adapter.getMediaType() - 1; + Handler handler = mUiHandler; + handler.sendMessage(handler.obtainMessage(MSG_COMMIT_QUERY, index, 0, adapter.query())); + break; + } + case MSG_COMMIT_QUERY: { + int index = message.arg1; + mAdapters[index].commitQuery(message.obj); + int pos; + if (mSavedPositions == null) { + pos = 0; + } else { + pos = mSavedPositions[index]; + mSavedPositions[index] = 0; + } + mLists[index].setSelection(pos); + break; + } + case MSG_SAVE_SORT: { + MediaAdapter adapter = (MediaAdapter)message.obj; + SharedPreferences.Editor editor = PlaybackService.getSettings(mActivity).edit(); + editor.putInt(String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()), adapter.getSortMode()); + editor.commit(); + break; + } + case MSG_REQUEST_REQUERY: + requestRequery((LibraryAdapter)message.obj); + break; + default: + return false; + } + + return true; + } + + /** + * Requery the given adapter. If it is the current adapter, requery + * immediately. Otherwise, mark the adapter as needing a requery and requery + * when its tab is selected. + * + * Must be called on the UI thread. + */ + public void requestRequery(LibraryAdapter adapter) + { + if (adapter == mCurrentAdapter) { + postRunQuery(adapter); + } else { + mRequeryNeeded[adapter.getMediaType() - 1] = true; + // Clear the data for non-visible adapters (so we don't show the old + // data briefly when we later switch to that adapter) + adapter.clear(); + } + } + + /** + * Call {@link LibraryActivity#requestRequery(LibraryAdapter)} on the UI + * thread. + * + * @param adapter The adapter, passed to requestRequery. + */ + public void postRequestRequery(LibraryAdapter adapter) + { + Handler handler = mUiHandler; + handler.sendMessage(handler.obtainMessage(MSG_REQUEST_REQUERY, adapter)); + } + + /** + * Schedule a query to be run for the given adapter on the worker thread. + * + * @param adapter The adapter to run the query for. + */ + private void postRunQuery(LibraryAdapter adapter) + { + mRequeryNeeded[adapter.getMediaType() - 1] = false; + Handler handler = mWorkerHandler; + handler.removeMessages(MSG_RUN_QUERY, adapter); + handler.sendMessage(handler.obtainMessage(MSG_RUN_QUERY, adapter)); + } + + /** + * Requery the adapter at the given position if it exists and needs a requery. + * + * @param position An index in mAdapters. + */ + private void requeryIfNeeded(int position) + { + LibraryAdapter adapter = mAdapters[position]; + if (adapter != null && mRequeryNeeded[position]) { + postRunQuery(adapter); + } + } + + /** + * Invalidate the data for all adapters. + */ + public void invalidateData() + { + for (LibraryAdapter adapter : mAdapters) { + if (adapter != null) { + postRequestRequery(adapter); + } + } + } + + /** + * Set the saved sort mode for the given adapter. The adapter should + * be re-queried after calling this. + * + * @param adapter The adapter to load for. + */ + public void loadSortOrder(MediaAdapter adapter) + { + String key = String.format("sort_%d_%d", adapter.getMediaType(), adapter.getLimiterType()); + int def = adapter.getDefaultSortMode(); + int sort = PlaybackService.getSettings(mActivity).getInt(key, def); + adapter.setSortMode(sort); + } + + /** + * Set the sort mode for the current adapter. Current adapter must be a + * MediaAdapter. Saves this sort mode to preferences and updates the list + * associated with the adapter to display the new sort mode. + * + * @param mode The sort mode. See {@link MediaAdapter#setSortMode(int)} for + * details. + */ + public void setSortMode(int mode) + { + MediaAdapter adapter = (MediaAdapter)mCurrentAdapter; + + adapter.setSortMode(mode); + requestRequery(adapter); + + // Force a new FastScroller to be created so the scroll sections + // are updated. + ListView view = mLists[mCurrentPage]; + view.setFastScrollEnabled(false); + view.setFastScrollEnabled(true); + + Handler handler = mWorkerHandler; + handler.sendMessage(handler.obtainMessage(MSG_SAVE_SORT, adapter)); + } + + /** + * Set a new filter on all the adapters. + */ + public void setFilter(String text) + { + if (text.length() == 0) + text = null; + + mFilter = text; + for (LibraryAdapter adapter : mAdapters) { + if (adapter != null) { + adapter.setFilter(text); + requestRequery(adapter); + } + } + } + + @Override + public void onPageScrollStateChanged(int state) + { + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + } + + @Override + public void onPageSelected(int position) + { + // onPageSelected and setPrimaryItem are called in similar cases, and it + // would be nice to use just one of them, but each has caveats: + // - onPageSelected isn't called when the ViewPager is first + // initialized + // - setPrimaryItem isn't called until scrolling is complete, which + // makes tab bar and limiter updates look bad + // So we use both. + setPrimaryItem(null, position, null); + } +}