com.cyanogenmod.eleven.ui.activities.SearchActivity.java Source code

Java tutorial

Introduction

Here is the source code for com.cyanogenmod.eleven.ui.activities.SearchActivity.java

Source

/*
 * Copyright (C) 2012 Andrew Neal
 * Copyright (C) 2014 The CyanogenMod Project
 * Licensed under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

package com.cyanogenmod.eleven.ui.activities;

import android.app.ActionBar;
import android.app.SearchManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;

import com.cyanogenmod.eleven.Config;
import com.cyanogenmod.eleven.IElevenService;
import com.cyanogenmod.eleven.R;
import com.cyanogenmod.eleven.adapters.SummarySearchAdapter;
import com.cyanogenmod.eleven.loaders.WrappedAsyncTaskLoader;
import com.cyanogenmod.eleven.menu.FragmentMenuItems;
import com.cyanogenmod.eleven.model.AlbumArtistDetails;
import com.cyanogenmod.eleven.model.SearchResult;
import com.cyanogenmod.eleven.model.SearchResult.ResultType;
import com.cyanogenmod.eleven.provider.SearchHistory;
import com.cyanogenmod.eleven.recycler.RecycleHolder;
import com.cyanogenmod.eleven.sectionadapter.SectionAdapter;
import com.cyanogenmod.eleven.sectionadapter.SectionCreator;
import com.cyanogenmod.eleven.sectionadapter.SectionCreator.SimpleListLoader;
import com.cyanogenmod.eleven.sectionadapter.SectionListContainer;
import com.cyanogenmod.eleven.utils.ApolloUtils;
import com.cyanogenmod.eleven.utils.MusicUtils;
import com.cyanogenmod.eleven.utils.MusicUtils.ServiceToken;
import com.cyanogenmod.eleven.utils.NavUtils;
import com.cyanogenmod.eleven.utils.PopupMenuHelper;
import com.cyanogenmod.eleven.utils.SectionCreatorUtils;
import com.cyanogenmod.eleven.utils.SectionCreatorUtils.IItemCompare;
import com.cyanogenmod.eleven.widgets.IPopupMenuCallback;
import com.cyanogenmod.eleven.widgets.LoadingEmptyContainer;
import com.cyanogenmod.eleven.widgets.NoResultsContainer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;

import static android.view.View.OnTouchListener;
import static com.cyanogenmod.eleven.utils.MusicUtils.mService;

/**
 * Provides the search interface for Apollo.
 * 
 * @author Andrew Neal (andrewdneal@gmail.com)
 */
public class SearchActivity extends FragmentActivity implements LoaderCallbacks<SectionListContainer<SearchResult>>,
        OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection, OnTouchListener {
    /**
     * Loading delay of 500ms so we don't flash the screen too much when loading new searches
     */
    private static int LOADING_DELAY = 500;

    /**
     * Identifier for the search loader
     */
    private static int SEARCH_LOADER = 0;

    /**
     * Identifier for the search history loader
     */
    private static int HISTORY_LOADER = 1;

    /**
     * The service token
     */
    private ServiceToken mToken;

    /**
     * The query
     */
    private String mFilterString;

    /**
     * List view
     */
    private ListView mListView;

    /**
     * Used the filter the user's music
     */
    private SearchView mSearchView;

    /**
     * IME manager
     */
    private InputMethodManager mImm;

    /**
     * The view that container the no search results text and the loading progress bar
     */
    private LoadingEmptyContainer mLoadingEmptyContainer;

    /**
     * List view adapter
     */
    private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;

    /**
     * boolean tracking whether this is the search level when the user first enters search
     * or if the user has clicked show all
     */
    private boolean mTopLevelSearch;

    /**
     * If the user has clicked show all, this tells us what type (Artist, Album, etc)
     */
    private ResultType mSearchType;

    /**
     * Search History loader callback
     */
    private SearchHistoryCallback mSearchHistoryCallback;

    /**
     * List view
     */
    private ListView mSearchHistoryListView;

    /**
     * This tracks our current visible state between the different views
      */
    enum VisibleState {
        SearchHistory, Empty, SearchResults, Loading,
    }

    private VisibleState mCurrentState;

    /**
     * Handler for posting runnables
     */
    private Handler mHandler;

    /**
     * A runnable to show the loading view that will be posted with a delay to prevent flashing
     */
    private Runnable mLoadingRunnable;

    /**
     * Flag used to track if we are quitting so we don't flash loaders while finishing the activity
     */
    private boolean mQuitting = false;

    /**
     * Pop up menu helper
     */
    private PopupMenuHelper mPopupMenuHelper;

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPopupMenuHelper = new PopupMenuHelper(this, getSupportFragmentManager()) {
            private SearchResult mSelectedItem;

            @Override
            public PopupMenuType onPreparePopupMenu(int position) {
                mSelectedItem = mAdapter.getTItem(position);

                return PopupMenuType.SearchResult;
            }

            @Override
            protected long[] getIdList() {
                switch (mSelectedItem.mType) {
                case Artist:
                    return MusicUtils.getSongListForArtist(SearchActivity.this, mSelectedItem.mId);
                case Album:
                    return MusicUtils.getSongListForAlbum(SearchActivity.this, mSelectedItem.mId);
                case Song:
                    return new long[] { mSelectedItem.mId };
                case Playlist:
                    return MusicUtils.getSongListForPlaylist(SearchActivity.this, mSelectedItem.mId);
                default:
                    return null;
                }
            }

            @Override
            protected long getSourceId() {
                return mSelectedItem.mId;
            }

            @Override
            protected Config.IdType getSourceType() {
                return mSelectedItem.mType.getSourceType();
            }

            @Override
            protected void updateMenuIds(PopupMenuType type, TreeSet<Integer> set) {
                super.updateMenuIds(type, set);

                if (mSelectedItem.mType == ResultType.Album) {
                    set.add(FragmentMenuItems.MORE_BY_ARTIST);
                }
            }

            @Override
            protected String getArtistName() {
                return mSelectedItem.mArtist;
            }
        };

        // Fade it in
        overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);

        // Control the media volume
        setVolumeControlStream(AudioManager.STREAM_MUSIC);

        // Bind Apollo's service
        mToken = MusicUtils.bindToService(this, this);

        // Set the layout
        setContentView(R.layout.activity_search);

        // get the input method manager
        mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);

        // Initialize the adapter
        SummarySearchAdapter adapter = new SummarySearchAdapter(this);
        mAdapter = new SectionAdapter<SearchResult, SummarySearchAdapter>(this, adapter);
        // Set the prefix
        mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
        mAdapter.setupHeaderParameters(R.layout.list_search_header, false);
        mAdapter.setupFooterParameters(R.layout.list_search_footer, true);
        mAdapter.setPopupMenuClickedListener(new IPopupMenuCallback.IListener() {
            @Override
            public void onPopupMenuClicked(View v, int position) {
                mPopupMenuHelper.showPopupMenu(v, position);
            }
        });

        mLoadingEmptyContainer = (LoadingEmptyContainer) findViewById(R.id.loading_empty_container);
        // setup the no results container
        NoResultsContainer noResults = mLoadingEmptyContainer.getNoResultsContainer();
        noResults.setMainText(R.string.empty_search);
        noResults.setSecondaryText(R.string.empty_search_check);

        initListView();

        // setup handler and runnable
        mHandler = new Handler();
        mLoadingRunnable = new Runnable() {
            @Override
            public void run() {
                setState(VisibleState.Loading);
            }
        };

        // Theme the action bar
        final ActionBar actionBar = getActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);

        // Get the query String
        mFilterString = getIntent().getStringExtra(SearchManager.QUERY);

        // if we have a non-empty search string, this is a 2nd lvl search
        if (!TextUtils.isEmpty(mFilterString)) {
            mTopLevelSearch = false;

            // get the search type to filter by
            int type = getIntent().getIntExtra(SearchManager.SEARCH_MODE, -1);
            if (type >= 0 && type < ResultType.values().length) {
                mSearchType = ResultType.values()[type];
            }

            int resourceId = 0;
            switch (mSearchType) {
            case Artist:
                resourceId = R.string.search_title_artists;
                break;
            case Album:
                resourceId = R.string.search_title_albums;
                break;
            case Playlist:
                resourceId = R.string.search_title_playlists;
                break;
            case Song:
                resourceId = R.string.search_title_songs;
                break;
            }
            actionBar.setTitle(getString(resourceId, mFilterString).toUpperCase());
            actionBar.setDisplayHomeAsUpEnabled(true);

            // Set the prefix
            mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);

            // Start the loader for the query
            getSupportLoaderManager().initLoader(SEARCH_LOADER, null, this);
        } else {
            mTopLevelSearch = true;
            mSearchHistoryCallback = new SearchHistoryCallback();

            // Start the loader for the search history
            getSupportLoaderManager().initLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
        }

        // set the background on the root view
        getWindow().getDecorView().getRootView()
                .setBackgroundColor(getResources().getColor(R.color.background_color));
    }

    /**
     * Sets up the list view
     */
    private void initListView() {
        // Initialize the grid
        mListView = (ListView) findViewById(R.id.list_base);
        // Set the data behind the list
        mListView.setAdapter(mAdapter);
        // Release any references to the recycled Views
        mListView.setRecyclerListener(new RecycleHolder());
        // Show the albums and songs from the selected artist
        mListView.setOnItemClickListener(this);
        // To help make scrolling smooth
        mListView.setOnScrollListener(this);
        // sets the touch listener
        mListView.setOnTouchListener(this);
        // If we setEmptyView with mLoadingEmptyContainer it causes a crash in DragSortListView
        // when updating the search.  For now let's manually toggle visibility and come back
        // to this later
        //mListView.setEmptyView(mLoadingEmptyContainer);

        // load the search history list view
        mSearchHistoryListView = (ListView) findViewById(R.id.list_search_history);
        mSearchHistoryListView.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                String searchItem = (String) mSearchHistoryListView.getAdapter().getItem(position);
                mSearchView.setQuery(searchItem, true);
            }
        });
        mSearchHistoryListView.setOnTouchListener(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id, final Bundle args) {
        IItemCompare<SearchResult> comparator = null;

        // prep the loader in case the query takes a long time
        setLoading();

        // if we are at the top level, create a comparator to separate the different types into
        // their own sections (artists, albums, etc)
        if (mTopLevelSearch) {
            comparator = SectionCreatorUtils.createSearchResultComparison(this);
        }

        return new SectionCreator<SearchResult>(this, new SummarySearchLoader(this, mFilterString, mSearchType),
                comparator);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onCreateOptionsMenu(final Menu menu) {
        // if we are not a top level search view, we do not need to create the search fields
        if (!mTopLevelSearch) {
            return super.onCreateOptionsMenu(menu);
        }

        // Search view
        getMenuInflater().inflate(R.menu.search, menu);

        // Filter the list the user is looking it via SearchView
        MenuItem searchItem = menu.findItem(R.id.menu_search);
        mSearchView = (SearchView) searchItem.getActionView();
        mSearchView.setOnQueryTextListener(this);
        mSearchView.setQueryHint(getString(R.string.searchHint).toUpperCase());

        // The SearchView has no way for you to customize or get access to the search icon in a
        // normal fashion, so we need to manually look for the icon and change the
        // layout params to hide it
        mSearchView.setIconifiedByDefault(false);
        mSearchView.setIconified(false);
        int searchButtonId = getResources().getIdentifier("android:id/search_mag_icon", null, null);
        ImageView searchIcon = (ImageView) mSearchView.findViewById(searchButtonId);
        searchIcon.setLayoutParams(new LinearLayout.LayoutParams(0, 0));

        searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
            @Override
            public boolean onMenuItemActionExpand(MenuItem item) {
                return true;
            }

            @Override
            public boolean onMenuItemActionCollapse(MenuItem item) {
                quit();
                return false;
            }
        });

        menu.findItem(R.id.menu_search).expandActionView();

        return super.onCreateOptionsMenu(menu);
    }

    private void quit() {
        mQuitting = true;
        finish();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Unbind from the service
        if (mService != null) {
            MusicUtils.unbindFromService(mToken);
            mToken = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
            quit();
            return true;
        default:
            break;
        }
        return super.onOptionsItemSelected(item);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoadFinished(final Loader<SectionListContainer<SearchResult>> loader,
            final SectionListContainer<SearchResult> data) {
        // Check for any errors
        if (data.mListResults.isEmpty()) {
            // clear the adapter
            mAdapter.clear();
            // show the empty state
            setState(VisibleState.Empty);
        } else {
            // Set the data
            mAdapter.setData(data);
            // show the search results
            setState(VisibleState.SearchResults);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoaderReset(final Loader<SectionListContainer<SearchResult>> loader) {
        mAdapter.unload();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onScrollStateChanged(final AbsListView view, final int scrollState) {
        // Pause disk cache access to ensure smoother scrolling
        if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
            mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
        } else {
            mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
            mAdapter.notifyDataSetChanged();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onQueryTextSubmit(final String query) {
        // simulate an on query text change
        onQueryTextChange(query);
        // hide the input manager
        hideInputManager();

        return true;
    }

    public void hideInputManager() {
        // When the search is "committed" by the user, then hide the keyboard so
        // the user can more easily browse the list of results.
        if (mSearchView != null) {
            if (mImm != null) {
                mImm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
            }
            mSearchView.clearFocus();

            // add our search string
            SearchHistory.getInstance(this).addSearchString(mFilterString);
        }
    }

    /**
     * This posts a delayed for showing the loading screen.  The reason for the delayed is we
     * don't want to flash the loading icon very often since searches usually are pretty fast
     */
    public void setLoading() {
        if (mCurrentState != VisibleState.Loading) {
            if (!mHandler.hasCallbacks(mLoadingRunnable)) {
                mHandler.postDelayed(mLoadingRunnable, LOADING_DELAY);
            }
        }
    }

    /**
     * Sets the currently visible view
     * @param state the current visible state
     */
    public void setState(VisibleState state) {
        // remove any delayed runnables.  This has to be before mCurrentState == state
        // in case the state doesn't change but we've created a loading runnable
        mHandler.removeCallbacks(mLoadingRunnable);

        // if we are already looking at view already, just quit
        if (mCurrentState == state) {
            return;
        }

        mCurrentState = state;

        mSearchHistoryListView.setVisibility(View.INVISIBLE);
        mListView.setVisibility(View.INVISIBLE);
        mLoadingEmptyContainer.setVisibility(View.INVISIBLE);

        switch (mCurrentState) {
        case SearchHistory:
            mSearchHistoryListView.setVisibility(View.VISIBLE);
            break;
        case SearchResults:
            mListView.setVisibility(View.VISIBLE);
            break;
        case Empty:
            mLoadingEmptyContainer.setVisibility(View.VISIBLE);
            mLoadingEmptyContainer.showNoResults();
            break;
        case Loading:
            mLoadingEmptyContainer.setVisibility(View.VISIBLE);
            mLoadingEmptyContainer.showLoading();
            break;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean onQueryTextChange(final String newText) {
        if (mQuitting) {
            return true;
        }

        if (TextUtils.isEmpty(newText)) {
            if (!TextUtils.isEmpty(mFilterString)) {
                mFilterString = "";
                getSupportLoaderManager().restartLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
                getSupportLoaderManager().destroyLoader(SEARCH_LOADER);
            }

            return true;
        }

        // if the strings are the same, return
        if (newText.equals(mFilterString)) {
            return true;
        }

        // Called when the action bar search text has changed. Update
        // the search filter, and restart the loader to do a new query
        // with this filter.
        mFilterString = newText;
        // Set the prefix
        mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
        getSupportLoaderManager().restartLoader(SEARCH_LOADER, null, this);
        getSupportLoaderManager().destroyLoader(HISTORY_LOADER);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) {
        if (mAdapter.isSectionFooter(position)) {
            // since a footer should be after a list item by definition, let's look up the type
            // of the previous item
            SearchResult item = mAdapter.getTItem(position - 1);
            Intent intent = new Intent(this, SearchActivity.class);
            intent.putExtra(SearchManager.QUERY, mFilterString);
            intent.putExtra(SearchManager.SEARCH_MODE, item.mType.ordinal());
            startActivity(intent);
        } else {
            SearchResult item = mAdapter.getTItem(position);
            switch (item.mType) {
            case Artist:
                NavUtils.openArtistProfile(this, item.mArtist);
                break;
            case Album:
                NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
                break;
            case Playlist:
                NavUtils.openPlaylist(this, item.mId, item.mTitle);
                break;
            case Song:
                // If it's a song, play it and leave
                final long[] list = new long[] { item.mId };
                MusicUtils.playAll(this, list, 0, -1, Config.IdType.NA, false);
                break;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onServiceConnected(final ComponentName name, final IBinder service) {
        mService = IElevenService.Stub.asInterface(service);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onServiceDisconnected(final ComponentName name) {
        mService = null;
    }

    /**
     * This class loads a search result summary of items
     */
    private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
        private final String mQuery;
        private final ResultType mSearchType;

        public SummarySearchLoader(final Context context, final String query, final ResultType searchType) {
            super(context);
            mQuery = query;
            mSearchType = searchType;
        }

        /**
         * This creates a search result given the data at the cursor position
         * @param cursor at the position for the item
         * @param type the type of item to create
         * @return the search result
         */
        protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
            SearchResult item = null;

            switch (type) {
            case Playlist:
                item = SearchResult.createPlaylistResult(cursor);
                item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
                break;
            case Song:
                item = SearchResult.createSearchResult(cursor);
                if (item != null) {
                    AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(), item.mId);
                    if (details != null) {
                        item.mArtist = details.mArtistName;
                        item.mAlbum = details.mAlbumName;
                        item.mAlbumId = details.mAlbumId;
                    }
                }
                break;
            case Album:
            case Artist:
            default:
                item = SearchResult.createSearchResult(cursor);
                break;
            }

            return item;
        }

        @Override
        public List<SearchResult> loadInBackground() {
            // if we are doing a specific type search, run that one
            if (mSearchType != null && mSearchType != ResultType.Unknown) {
                return runSearchForType();
            }

            return runGenericSearch();
        }

        /**
         * This creates a search for a specific type given a filter string.  This will return the
         * full list of results that matches those two requirements
         * @return the results for that search
         */
        protected List<SearchResult> runSearchForType() {
            ArrayList<SearchResult> results = new ArrayList<SearchResult>();
            Cursor cursor = null;
            try {
                if (mSearchType == ResultType.Playlist) {
                    cursor = makePlaylistSearchCursor(getContext(), mQuery);
                } else {
                    cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
                }

                // pre-cache this index
                final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);

                if (cursor != null && cursor.moveToFirst()) {
                    do {
                        boolean addResult = true;

                        if (mSearchType != ResultType.Playlist) {
                            // get the result type
                            ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
                            if (type != mSearchType) {
                                addResult = false;
                            }
                        }

                        if (addResult) {
                            results.add(createSearchResult(cursor, mSearchType));
                        }
                    } while (cursor.moveToNext());
                }

            } finally {
                if (cursor != null) {
                    cursor.close();
                    cursor = null;
                }
            }

            return results;
        }

        /**
         * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
         * type
         * @return the results for that search
         */
        public List<SearchResult> runGenericSearch() {
            ArrayList<SearchResult> results = new ArrayList<SearchResult>();
            // number of types to query for
            final int numTypes = ResultType.getNumTypes();

            // number of results we want
            final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;

            // current number of results we have
            int numResultsAdded = 0;

            // count for each result type
            int[] numOfEachType = new int[numTypes];

            // search playlists first
            Cursor playlistCursor = makePlaylistSearchCursor(getContext(), mQuery);
            if (playlistCursor != null && playlistCursor.moveToFirst()) {
                do {
                    // create the item
                    SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
                    /// add the results
                    numResultsAdded++;
                    results.add(item);
                } while (playlistCursor.moveToNext() && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);

                // because we deal with playlists separately,
                // just mark that we have the full # of playlists
                // so that logic later can quit out early if full
                numResultsAdded = Config.SEARCH_NUM_RESULTS_TO_GET;

                // close the cursor
                playlistCursor.close();
                playlistCursor = null;
            }

            // do fancy audio search
            Cursor cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);

            // pre-cache this index
            final int mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);

            // walk through the cursor
            if (cursor != null && cursor.moveToFirst()) {
                do {
                    // get the result type
                    ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);

                    // if we still need this type
                    if (numOfEachType[type.ordinal()] < Config.SEARCH_NUM_RESULTS_TO_GET) {
                        // get the search result
                        SearchResult item = createSearchResult(cursor, type);

                        if (item != null) {
                            // add it
                            results.add(item);
                            numOfEachType[type.ordinal()]++;
                            numResultsAdded++;

                            // if we have enough then quit
                            if (numResultsAdded >= numResultsNeeded) {
                                break;
                            }
                        }
                    }
                } while (cursor.moveToNext());

                cursor.close();
                cursor = null;
            }

            // sort our results
            Collections.sort(results, SearchResult.COMPARATOR);

            return results;
        }

        public static Cursor makePlaylistSearchCursor(final Context context, final String searchTerms) {
            if (TextUtils.isEmpty(searchTerms)) {
                return null;
            }

            // trim out special characters like % or \ as well as things like "a" "and" etc
            String trimmedSearchTerms = MusicUtils.getTrimmedName(searchTerms);

            if (TextUtils.isEmpty(trimmedSearchTerms)) {
                return null;
            }

            String[] keywords = trimmedSearchTerms.split(" ");

            // prep the keyword for like search
            for (int i = 0; i < keywords.length; i++) {
                keywords[i] = "%" + keywords[i] + "%";
            }

            String where = "";

            // make the where clause
            for (int i = 0; i < keywords.length; i++) {
                if (i == 0) {
                    where = "name LIKE ?";
                } else {
                    where += " AND name LIKE ?";
                }
            }

            return context.getContentResolver().query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
                    new String[] {
                            /* 0 */
                            BaseColumns._ID,
                            /* 1 */
                            MediaStore.Audio.PlaylistsColumns.NAME },
                    where, keywords, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
        }
    }

    /**
     * Loads the search history in the background and creates an array adapter
     */
    public static class SearchHistoryLoader extends WrappedAsyncTaskLoader<ArrayAdapter<String>> {
        public SearchHistoryLoader(Context context) {
            super(context);
        }

        @Override
        public ArrayAdapter<String> loadInBackground() {
            ArrayList<String> strings = SearchHistory.getInstance(getContext()).getRecentSearches();
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(), R.layout.list_item_search_history,
                    R.id.line_one);
            adapter.addAll(strings);
            return adapter;
        }
    }

    /**
     * This handles the Loader callbacks for the search history
     */
    public class SearchHistoryCallback implements LoaderCallbacks<ArrayAdapter<String>> {
        @Override
        public Loader<ArrayAdapter<String>> onCreateLoader(int i, Bundle bundle) {
            // prep the loader in case the query takes a long time
            setLoading();

            return new SearchHistoryLoader(SearchActivity.this);
        }

        @Override
        public void onLoadFinished(Loader<ArrayAdapter<String>> searchHistoryAdapterLoader,
                ArrayAdapter<String> searchHistoryAdapter) {
            // show the search history
            setState(VisibleState.SearchHistory);

            mSearchHistoryListView.setAdapter(searchHistoryAdapter);
        }

        @Override
        public void onLoaderReset(Loader<ArrayAdapter<String>> cursorAdapterLoader) {
            ((ArrayAdapter) mSearchHistoryListView.getAdapter()).clear();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount,
            final int totalItemCount) {
        // Nothing to do
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        hideInputManager();
        return false;
    }
}