ch.blinkenlights.android.vanilla.LibraryPagerAdapter.java Source code

Java tutorial

Introduction

Here is the source code for ch.blinkenlights.android.vanilla.LibraryPagerAdapter.java

Source

/*
 * Copyright (C) 2012 Christopher Eby <kreed@kreed.org>
 * Copyright (C) 2015-2018 Adrian Ulrich <adrian@blinkenlights.ch>
 *
 * 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 ch.blinkenlights.android.vanilla;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.util.LruCache;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.Arrays;
import java.util.ArrayList;

/**
 * PagerAdapter that manages the library media ListViews.
 */
public class LibraryPagerAdapter extends PagerAdapter implements Handler.Callback, ViewPager.OnPageChangeListener,
        View.OnCreateContextMenuListener, AdapterView.OnItemClickListener {
    /**
     * The number of unique list types. The number of visible lists may be
     * smaller.
     */
    public static final int MAX_ADAPTER_COUNT = MediaUtils.TYPE_COUNT;
    /**
     * The human-readable title for each list. The positions correspond to the
     * MediaUtils ids, so e.g. TITLES[MediaUtils.TYPE_SONG] = R.string.songs
     */
    public static final int[] TITLES = { R.string.artists, R.string.albums, R.string.songs, R.string.playlists,
            R.string.genres, R.string.albumartists, R.string.composers, R.string.files };
    /**
     * Default tab order.
     */
    public static final int[] DEFAULT_TAB_ORDER = { MediaUtils.TYPE_ARTIST, MediaUtils.TYPE_ALBARTIST,
            MediaUtils.TYPE_COMPOSER, MediaUtils.TYPE_ALBUM, MediaUtils.TYPE_SONG, MediaUtils.TYPE_PLAYLIST,
            MediaUtils.TYPE_GENRE, MediaUtils.TYPE_FILE };
    /**
     * The default visibility of tabs
     */
    public static final boolean[] DEFAULT_TAB_VISIBILITY = { true, false, false, true, true, true, true, true };

    /**
     * The user-chosen tab order.
     */
    int[] mTabOrder;
    /**
     * The number of visible tabs.
     */
    private int mTabCount;
    /**
     * The ListView for each adapter. Each index corresponds to that list's
     * MediaUtils id.
     */
    private final ListView[] mLists = new ListView[MAX_ADAPTER_COUNT];
    /**
     * The adapters. Each index corresponds to that adapter's MediaUtils id.
     */
    public LibraryAdapter[] mAdapters = new LibraryAdapter[MAX_ADAPTER_COUNT];
    /**
     * Whether the adapter corresponding to each index has stale data.
     */
    private final boolean[] mRequeryNeeded = new boolean[MAX_ADAPTER_COUNT];
    /**
     * The artist adapter instance, also stored at mAdapters[MediaUtils.TYPE_ARTIST].
     */
    private MediaAdapter mArtistAdapter;
    /**
     * The albumartist adapter instance, also stored at mAdapters[MediaUtils.TYPE_ALBART].
     */
    private MediaAdapter mAlbArtAdapter;
    /**
     * The composer adapter instance, also stored at mAdapters[MediaUtils.TYPE_COMPOSER].
     */
    private MediaAdapter mComposerAdapter;
    /**
     * The album adapter instance, also stored at mAdapters[MediaUtils.TYPE_ALBUM].
     */
    private MediaAdapter mAlbumAdapter;
    /**
     * The song adapter instance, also stored at mAdapters[MediaUtils.TYPE_SONG].
     */
    private MediaAdapter mSongAdapter;
    /**
     * The playlist adapter instance, also stored at mAdapters[MediaUtils.TYPE_PLAYLIST].
     */
    MediaAdapter mPlaylistAdapter;
    /**
     * The genre adapter instance, also stored at mAdapters[MediaUtils.TYPE_GENRE].
     */
    private MediaAdapter mGenreAdapter;
    /**
     * The file adapter instance, also stored at mAdapters[MediaUtils.TYPE_FILE].
     */
    private FileSystemAdapter mFilesAdapter;
    /**
     * LRU cache holding the last scrolling position of all adapter views
     */
    private static AdaperPositionLruCache sLruAdapterPos;
    /**
     * 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 mPendingArtistLimiter;
    /**
     * A limiter that should be set when the albumartist adapter is created.
     */
    private Limiter mPendingAlbArtLimiter;
    /**
     * A limiter that should be set when the composer adapter is created.
     */
    private Limiter mPendingComposerLimiter;
    /**
     * 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;
    /**
     * 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 running 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;
    /**
     * A list of header rows which require test updates
     */
    private ArrayList<DraggableRow> mHeaderViews = new ArrayList();
    /**
     * The current filter text, or null if none.
     */
    private String mFilter;

    /**
     * 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) {
        if (sLruAdapterPos == null)
            sLruAdapterPos = new AdaperPositionLruCache(32);
        mActivity = activity;
        mUiHandler = new Handler(this);
        mWorkerHandler = new Handler(workerLooper, this);
        mCurrentPage = -1;
    }

    /**
     * Load the tab order from SharedPreferences.
     *
     * @return True if order has changed.
     */
    public boolean loadTabOrder() {
        String in = PlaybackService.getSettings(mActivity).getString(PrefKeys.TAB_ORDER, PrefDefaults.TAB_ORDER);
        int[] order = new int[MAX_ADAPTER_COUNT];
        int count = 0;
        if (in != null && in.length() == MAX_ADAPTER_COUNT) {
            char[] chars = in.toCharArray();
            order = new int[MAX_ADAPTER_COUNT];
            for (int i = 0; i != MAX_ADAPTER_COUNT; ++i) {
                char v = chars[i];
                if (v >= 128) {
                    v -= 128;
                    if (v >= MediaUtils.TYPE_COUNT) {
                        // invalid media type, ignore all data
                        count = 0;
                        break;
                    }
                    order[count++] = v;
                }
            }
        }

        // set default tabs if none were loaded
        if (count == 0) {
            for (int i = 0; i != MAX_ADAPTER_COUNT; i++) {
                if (DEFAULT_TAB_VISIBILITY[i])
                    order[count++] = DEFAULT_TAB_ORDER[i];
            }
        }

        if (count != mTabCount || !Arrays.equals(order, mTabOrder)) {
            mTabOrder = order;
            mTabCount = count;
            notifyDataSetChanged();
            computeExpansions();
            return true;
        }

        return false;
    }

    /**
     * Determines whether adapters should be expandable from the visibility of
     * the adapters each expands to. Also updates mSongsPosition/mAlbumsPositions.
     */
    public void computeExpansions() {
        if (mArtistAdapter != null)
            mArtistAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1
                    || getMediaTypePosition(MediaUtils.TYPE_ALBUM) != -1);
        if (mAlbumAdapter != null)
            mAlbumAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1);
        if (mGenreAdapter != null)
            mGenreAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1);
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        int type = mTabOrder[position];
        ListView view = mLists[type];

        if (view == null) {
            LibraryActivity activity = mActivity;
            LayoutInflater inflater = activity.getLayoutInflater();
            LibraryAdapter adapter;
            DraggableRow header = null;

            switch (type) {
            case MediaUtils.TYPE_ARTIST:
                adapter = mArtistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ARTIST, mPendingArtistLimiter,
                        activity);
                mArtistAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1
                        || getMediaTypePosition(MediaUtils.TYPE_ALBUM) != -1);
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            case MediaUtils.TYPE_ALBARTIST:
                adapter = mAlbArtAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBARTIST,
                        mPendingAlbArtLimiter, activity);
                mAlbArtAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1
                        || getMediaTypePosition(MediaUtils.TYPE_ALBUM) != -1);
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            case MediaUtils.TYPE_COMPOSER:
                adapter = mComposerAdapter = new MediaAdapter(activity, MediaUtils.TYPE_COMPOSER,
                        mPendingComposerLimiter, activity);
                mComposerAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1
                        || getMediaTypePosition(MediaUtils.TYPE_ALBUM) != -1);
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            case MediaUtils.TYPE_ALBUM:
                adapter = mAlbumAdapter = new MediaAdapter(activity, MediaUtils.TYPE_ALBUM, mPendingAlbumLimiter,
                        activity);
                mAlbumAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1);
                mPendingAlbumLimiter = null;
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            case MediaUtils.TYPE_SONG:
                adapter = mSongAdapter = new MediaAdapter(activity, MediaUtils.TYPE_SONG, mPendingSongLimiter,
                        activity);
                mPendingSongLimiter = null;
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            case MediaUtils.TYPE_PLAYLIST:
                adapter = mPlaylistAdapter = new MediaAdapter(activity, MediaUtils.TYPE_PLAYLIST, null, activity);
                break;
            case MediaUtils.TYPE_GENRE:
                adapter = mGenreAdapter = new MediaAdapter(activity, MediaUtils.TYPE_GENRE, null, activity);
                mGenreAdapter.setExpandable(getMediaTypePosition(MediaUtils.TYPE_SONG) != -1);
                break;
            case MediaUtils.TYPE_FILE:
                adapter = mFilesAdapter = new FileSystemAdapter(activity, mPendingFileLimiter);
                mPendingFileLimiter = null;
                header = (DraggableRow) inflater.inflate(R.layout.draggable_row, null);
                break;
            default:
                throw new IllegalArgumentException("Invalid media type: " + type);
            }

            view = (ListView) inflater.inflate(R.layout.listview, null);
            view.setOnCreateContextMenuListener(this);
            view.setOnItemClickListener(this);

            view.setTag(type);
            if (header != null) {
                header.setText(mHeaderText);
                header.setTag(new ViewHolder()); // behave like a normal library row
                view.addHeaderView(header);
                mHeaderViews.add(header);
            }
            view.setAdapter(adapter);

            loadSortOrder((SortableAdapter) adapter);

            adapter.setFilter(mFilter);

            mAdapters[type] = adapter;
            mLists[type] = view;
            mRequeryNeeded[type] = true;
        }

        requeryIfNeeded(type);
        container.addView(view);
        return view;
    }

    @Override
    public int getItemPosition(Object item) {
        int type = (Integer) ((ListView) item).getTag();
        int pos = getMediaTypePosition(type);
        return pos == -1 ? POSITION_NONE : pos;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return mActivity.getResources().getText(TITLES[mTabOrder[position]]);
    }

    @Override
    public int getCount() {
        return mTabCount;
    }

    @Override
    public boolean isViewFromObject(View view, Object object) {
        return view == object;
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        int type = mTabOrder[position];
        LibraryAdapter adapter = mAdapters[type];
        if (position != mCurrentPage || adapter != mCurrentAdapter) {
            requeryIfNeeded(type);
            mCurrentAdapter = adapter;
            mCurrentPage = position;
            mActivity.onPageChanged(position, adapter);
        }
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        Bundle in = (Bundle) state;
        mPendingArtistLimiter = (Limiter) in.getSerializable("limiter_artists");
        mPendingAlbArtLimiter = (Limiter) in.getSerializable("limiter_albumartists");
        mPendingComposerLimiter = (Limiter) in.getSerializable("limiter_composer");
        mPendingAlbumLimiter = (Limiter) in.getSerializable("limiter_albums");
        mPendingSongLimiter = (Limiter) in.getSerializable("limiter_songs");
        mPendingFileLimiter = (Limiter) in.getSerializable("limiter_files");
    }

    @Override
    public Parcelable saveState() {
        Bundle out = new Bundle(10);
        if (mArtistAdapter != null)
            out.putSerializable("limiter_artists", mArtistAdapter.getLimiter());
        if (mAlbArtAdapter != null)
            out.putSerializable("limiter_albumartists", mAlbArtAdapter.getLimiter());
        if (mComposerAdapter != null)
            out.putSerializable("limiter_composer", mComposerAdapter.getLimiter());
        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());

        maintainPosition();
        return out;
    }

    /**
     * Sets the text to be displayed in the first row of the artist, album, and
     * song lists.
     */
    public void setHeaderText(String text) {
        for (DraggableRow row : mHeaderViews) {
            row.setText(text);
        }
        mHeaderText = text;
    }

    /**
     * Clear a limiter.
     *
     * @param type Which type of limiter to clear.
     */
    public void clearLimiter(int type) {
        ArrayList<LibraryAdapter> targets = new ArrayList<LibraryAdapter>();

        maintainPosition();

        if (type == MediaUtils.TYPE_FILE) {
            if (mFilesAdapter == null) {
                mPendingFileLimiter = null;
            } else {
                targets.add(mFilesAdapter);
            }
        } else {
            if (mArtistAdapter == null) {
                mPendingArtistLimiter = null;
            } else {
                targets.add(mArtistAdapter);
            }
            if (mAlbArtAdapter == null) {
                mPendingAlbArtLimiter = null;
            } else {
                targets.add(mAlbArtAdapter);
            }
            if (mComposerAdapter == null) {
                mPendingComposerLimiter = null;
            } else {
                targets.add(mComposerAdapter);
            }
            if (mAlbumAdapter == null) {
                mPendingAlbumLimiter = null;
            } else {
                targets.add(mAlbumAdapter);
            }
            if (mSongAdapter == null) {
                mPendingSongLimiter = null;
            } else {
                targets.add(mSongAdapter);
            }
        }

        for (LibraryAdapter adapter : targets) {
            adapter.setLimiter(null);
            loadSortOrder((SortableAdapter) adapter);
            requestRequery(adapter);
        }
    }

    /**
     * Update the adapters with the given limiter.
     *
     * @param limiter The limiter to set.
     * @return The tab type that should be switched to to expand the row.
     */
    public int setLimiter(Limiter limiter) {
        int tab;
        ArrayList<LibraryAdapter> targets = new ArrayList<LibraryAdapter>();

        maintainPosition();

        switch (limiter.type) {
        case MediaUtils.TYPE_ALBUM:
            if (mSongAdapter == null) {
                mPendingSongLimiter = limiter;
            } else {
                targets.add(mSongAdapter);
            }
            tab = getMediaTypePosition(MediaUtils.TYPE_SONG);
            break;
        case MediaUtils.TYPE_ARTIST:
        case MediaUtils.TYPE_ALBARTIST:
        case MediaUtils.TYPE_COMPOSER:
            if (mAlbumAdapter == null) {
                mPendingAlbumLimiter = limiter;
            } else {
                targets.add(mAlbumAdapter);
            }
            if (mSongAdapter == null) {
                mPendingSongLimiter = limiter;
            } else {
                targets.add(mSongAdapter);
            }
            tab = getMediaTypePosition(MediaUtils.TYPE_ALBUM);
            if (tab == -1)
                tab = getMediaTypePosition(MediaUtils.TYPE_SONG);
            break;
        case MediaUtils.TYPE_GENRE:
            if (mArtistAdapter == null) {
                mPendingArtistLimiter = limiter;
            } else {
                targets.add(mArtistAdapter);
            }
            if (mAlbArtAdapter == null) {
                mPendingAlbArtLimiter = limiter;
            } else {
                targets.add(mAlbArtAdapter);
            }
            if (mComposerAdapter == null) {
                mPendingComposerLimiter = limiter;
            } else {
                targets.add(mComposerAdapter);
            }
            if (mAlbumAdapter == null) {
                mPendingAlbumLimiter = limiter;
            } else {
                targets.add(mAlbumAdapter);
            }
            if (mSongAdapter == null) {
                mPendingSongLimiter = limiter;
            } else {
                targets.add(mSongAdapter);
            }
            tab = getMediaTypePosition(MediaUtils.TYPE_ARTIST);
            if (tab == -1)
                tab = getMediaTypePosition(MediaUtils.TYPE_ALBUM);
            if (tab == -1)
                tab = getMediaTypePosition(MediaUtils.TYPE_SONG);
            break;
        case MediaUtils.TYPE_FILE:
            if (mFilesAdapter == null) {
                mPendingFileLimiter = limiter;
            } else {
                targets.add(mFilesAdapter);
                // forcefully jump to beginning - commit query might restore a saved position
                // but if it doesn't we would end up at the same scrolling position in a new
                // folder which is uncool.
                mLists[MediaUtils.TYPE_FILE].setSelection(0);
            }
            tab = getMediaTypePosition(MediaUtils.TYPE_FILE);
            break;
        default:
            throw new IllegalArgumentException("Unsupported limiter type: " + limiter.type);
        }

        for (LibraryAdapter adapter : targets) {
            adapter.setLimiter(limiter);
            loadSortOrder((SortableAdapter) adapter);
            requestRequery(adapter);
        }

        return tab;
    }

    /**
     * Saves the scrolling position of every visible limiter
     */
    private void maintainPosition() {
        for (int i = MAX_ADAPTER_COUNT; --i != -1;) {
            if (mAdapters[i] != null) {
                sLruAdapterPos.storePosition(mAdapters[i], mLists[i].getFirstVisiblePosition());
            }
        }
    }

    /**
     * Restores the saved scrolling position
     */
    private void restorePosition(int index) {
        // Restore scrolling position if present and valid
        Integer curPos = sLruAdapterPos.popPosition(mAdapters[index]);
        if (curPos != null && curPos < mLists[index].getCount())
            mLists[index].setSelection(curPos);
    }

    /**
     * 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();
    }

    /**
     * Returns the tab position of given media type, -1 if not visible.
     *
     * @return int
     */
    public int getMediaTypePosition(int type) {
        int[] order = mTabOrder;
        for (int i = mTabCount; --i != -1;) {
            if (order[i] == type)
                return i;
        }
        return -1;
    }

    /**
     * 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 LibraryPagerAdapter#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();
            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);
            restorePosition(index);
            break;
        }
        case MSG_SAVE_SORT: {
            SortableAdapter adapter = (SortableAdapter) message.obj;
            SharedPreferences.Editor editor = PlaybackService.getSettings(mActivity).edit();
            editor.putInt(adapter.getSortSettingsKey(), adapter.getSortMode());
            editor.apply();
            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()] = 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 LibraryPagerAdapter#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()] = false;
        Handler handler = mWorkerHandler;
        handler.removeMessages(MSG_RUN_QUERY, adapter);
        handler.sendMessage(handler.obtainMessage(MSG_RUN_QUERY, adapter));
    }

    /**
     * Requery the adapter of the given type if it exists and needs a requery.
     *
     * @param type One of MediaUtils.TYPE_*
     */
    private void requeryIfNeeded(int type) {
        LibraryAdapter adapter = mAdapters[type];
        if (adapter != null && mRequeryNeeded[type]) {
            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(SortableAdapter adapter) {
        String key = adapter.getSortSettingsKey();
        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) {
        SortableAdapter adapter = (SortableAdapter) mCurrentAdapter;
        if (mode == adapter.getSortMode())
            return;

        adapter.setSortMode(mode);
        requestRequery(mCurrentAdapter);

        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 if there is no scrolling to do
        // - 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);
    }

    /**
     * Creates the row data used by LibraryActivity.
     */
    private static Intent createHeaderIntent(View header) {
        int type = (Integer) ((View) header.getParent()).getTag(); // tag is set on parent view of header
        Intent intent = new Intent();
        intent.putExtra(LibraryAdapter.DATA_ID, LibraryAdapter.HEADER_ID);
        intent.putExtra(LibraryAdapter.DATA_TYPE, type);
        return intent;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        View targetView = info.targetView;
        Intent intent = info.id == LibraryAdapter.HEADER_ID ? createHeaderIntent(targetView)
                : mCurrentAdapter.createData(targetView);
        int type = (Integer) ((View) targetView.getParent()).getTag();

        if (type == MediaUtils.TYPE_FILE) {
            mFilesAdapter.onCreateContextMenu(menu, intent);
        } else {
            mActivity.onCreateContextMenu(menu, intent);
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        Intent intent = id == LibraryAdapter.HEADER_ID ? createHeaderIntent(view)
                : mCurrentAdapter.createData(view);
        int type = (Integer) ((View) view.getParent()).getTag();

        if (type == MediaUtils.TYPE_FILE) {
            mFilesAdapter.onItemClicked(intent);
        } else {
            mActivity.onItemClicked(intent);
        }
    }

    /**
     * Perform usability-related actions on pager and contained lists, e.g. highlight current song
     * or scroll to it if opted-in
     * @param song song that is currently playing, can be null
      */
    public void onSongChange(Song song) {
        if (mCurrentPage == -1) // no page active, nothing to do
            return;

        int type = mTabOrder[mCurrentPage];
        ListView view = mLists[type];
        if (view == null) // not initialized yet, nothing to do
            return;

        long id = MediaUtils.getCurrentIdForType(song, type);
        if (id == -1) // unknown type
            return;

        // scroll to song on song change if opted-in
        SharedPreferences sharedPrefs = PlaybackService.getSettings(mActivity);
        boolean shouldScroll = sharedPrefs.getBoolean(PrefKeys.ENABLE_SCROLL_TO_SONG,
                PrefDefaults.ENABLE_SCROLL_TO_SONG);
        if (shouldScroll) {
            int middlePos = (view.getFirstVisiblePosition() + view.getLastVisiblePosition()) / 2;
            for (int pos = 0; pos < view.getCount(); pos++) {
                if (view.getItemIdAtPosition(pos) == id) {
                    if (Math.abs(middlePos - pos) < 30) {
                        view.smoothScrollToPosition(pos);
                    } else {
                        view.setSelection(pos);
                    }
                    break;
                }
            }
        }
    }

    /**
     * LRU implementation: saves the adapter position
     */
    private class AdaperPositionLruCache extends LruCache<String, Integer> {
        public AdaperPositionLruCache(int size) {
            super(size);
        }

        public void storePosition(LibraryAdapter adapter, Integer val) {
            this.put(_k(adapter), val);
        }

        public Integer popPosition(LibraryAdapter adapter) {
            return this.remove(_k(adapter));
        }

        /**
         * Assemble internal cache key from adapter
         */
        private String _k(LibraryAdapter adapter) {
            String result = adapter.getMediaType() + "://";
            Limiter limiter = adapter.getLimiter();

            if (limiter != null) {
                for (String entry : limiter.names) {
                    result = result + entry + "/";
                }
            }
            return result;
        }

    }

}