2 * Copyright (C) 2012 Andrew Neal
3 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0
5 * (the "License"); you may not use this file except in compliance with the
6 * License. You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
8 * or agreed to in writing, software distributed under the License is
9 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
10 * KIND, either express or implied. See the License for the specific language
11 * governing permissions and limitations under the License.
14 package com.cyanogenmod.eleven.ui.activities;
16 import android.app.ActionBar;
17 import android.app.SearchManager;
18 import android.content.ComponentName;
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.ServiceConnection;
22 import android.database.Cursor;
23 import android.media.AudioManager;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.IBinder;
27 import android.provider.BaseColumns;
28 import android.provider.MediaStore;
29 import android.support.v4.app.FragmentActivity;
30 import android.support.v4.app.LoaderManager.LoaderCallbacks;
31 import android.support.v4.content.Loader;
32 import android.text.TextUtils;
33 import android.view.Menu;
34 import android.view.MenuItem;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.inputmethod.InputMethodManager;
38 import android.widget.AbsListView;
39 import android.widget.AbsListView.OnScrollListener;
40 import android.widget.AdapterView;
41 import android.widget.AdapterView.OnItemClickListener;
42 import android.widget.ArrayAdapter;
43 import android.widget.ImageView;
44 import android.widget.LinearLayout;
45 import android.widget.ListView;
46 import android.widget.SearchView;
47 import android.widget.SearchView.OnQueryTextListener;
49 import com.cyanogenmod.eleven.Config;
50 import com.cyanogenmod.eleven.IElevenService;
51 import com.cyanogenmod.eleven.R;
52 import com.cyanogenmod.eleven.adapters.SummarySearchAdapter;
53 import com.cyanogenmod.eleven.loaders.WrappedAsyncTaskLoader;
54 import com.cyanogenmod.eleven.menu.FragmentMenuItems;
55 import com.cyanogenmod.eleven.model.AlbumArtistDetails;
56 import com.cyanogenmod.eleven.model.SearchResult;
57 import com.cyanogenmod.eleven.model.SearchResult.ResultType;
58 import com.cyanogenmod.eleven.provider.SearchHistory;
59 import com.cyanogenmod.eleven.recycler.RecycleHolder;
60 import com.cyanogenmod.eleven.sectionadapter.SectionAdapter;
61 import com.cyanogenmod.eleven.sectionadapter.SectionCreator;
62 import com.cyanogenmod.eleven.sectionadapter.SectionCreator.SimpleListLoader;
63 import com.cyanogenmod.eleven.sectionadapter.SectionListContainer;
64 import com.cyanogenmod.eleven.utils.ApolloUtils;
65 import com.cyanogenmod.eleven.utils.MusicUtils;
66 import com.cyanogenmod.eleven.utils.MusicUtils.ServiceToken;
67 import com.cyanogenmod.eleven.utils.NavUtils;
68 import com.cyanogenmod.eleven.utils.PopupMenuHelper;
69 import com.cyanogenmod.eleven.utils.SectionCreatorUtils;
70 import com.cyanogenmod.eleven.utils.SectionCreatorUtils.IItemCompare;
71 import com.cyanogenmod.eleven.widgets.IPopupMenuCallback;
72 import com.cyanogenmod.eleven.widgets.LoadingEmptyContainer;
73 import com.cyanogenmod.eleven.widgets.NoResultsContainer;
75 import java.util.ArrayList;
76 import java.util.Collections;
77 import java.util.List;
78 import java.util.TreeSet;
80 import static android.view.View.OnTouchListener;
81 import static com.cyanogenmod.eleven.utils.MusicUtils.mService;
84 * Provides the search interface for Apollo.
86 * @author Andrew Neal (andrewdneal@gmail.com)
88 public class SearchActivity extends FragmentActivity implements
89 LoaderCallbacks<SectionListContainer<SearchResult>>,
90 OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection,
93 * Loading delay of 500ms so we don't flash the screen too much when loading new searches
95 private static int LOADING_DELAY = 500;
98 * Identifier for the search loader
100 private static int SEARCH_LOADER = 0;
103 * Identifier for the search history loader
105 private static int HISTORY_LOADER = 1;
110 private ServiceToken mToken;
115 private String mFilterString;
120 private ListView mListView;
123 * Used the filter the user's music
125 private SearchView mSearchView;
130 private InputMethodManager mImm;
133 * The view that container the no search results text and the loading progress bar
135 private LoadingEmptyContainer mLoadingEmptyContainer;
140 private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;
143 * boolean tracking whether this is the search level when the user first enters search
144 * or if the user has clicked show all
146 private boolean mTopLevelSearch;
149 * If the user has clicked show all, this tells us what type (Artist, Album, etc)
151 private ResultType mSearchType;
154 * Search History loader callback
156 private SearchHistoryCallback mSearchHistoryCallback;
161 private ListView mSearchHistoryListView;
164 * This tracks our current visible state between the different views
173 private VisibleState mCurrentState;
176 * Handler for posting runnables
178 private Handler mHandler;
181 * A runnable to show the loading view that will be posted with a delay to prevent flashing
183 private Runnable mLoadingRunnable;
186 * Flag used to track if we are quitting so we don't flash loaders while finishing the activity
188 private boolean mQuitting = false;
193 private PopupMenuHelper mPopupMenuHelper;
199 public void onCreate(final Bundle savedInstanceState) {
200 super.onCreate(savedInstanceState);
202 mPopupMenuHelper = new PopupMenuHelper(this, getSupportFragmentManager()) {
203 private SearchResult mSelectedItem;
206 public PopupMenuType onPreparePopupMenu(int position) {
207 mSelectedItem = mAdapter.getTItem(position);
209 return PopupMenuType.SearchResult;
213 protected long[] getIdList() {
214 switch (mSelectedItem.mType) {
216 return MusicUtils.getSongListForArtist(SearchActivity.this,
219 return MusicUtils.getSongListForAlbum(SearchActivity.this,
222 return new long[] { mSelectedItem.mId };
224 return MusicUtils.getSongListForPlaylist(SearchActivity.this,
232 protected long getSourceId() {
233 return mSelectedItem.mId;
237 protected Config.IdType getSourceType() {
238 return mSelectedItem.mType.getSourceType();
242 protected void updateMenuIds(PopupMenuType type, TreeSet<Integer> set) {
243 super.updateMenuIds(type, set);
245 if (mSelectedItem.mType == ResultType.Album) {
246 set.add(FragmentMenuItems.MORE_BY_ARTIST);
251 protected String getArtistName() {
252 return mSelectedItem.mArtist;
257 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
259 // Control the media volume
260 setVolumeControlStream(AudioManager.STREAM_MUSIC);
262 // Bind Apollo's service
263 mToken = MusicUtils.bindToService(this, this);
266 setContentView(R.layout.activity_search);
268 // get the input method manager
269 mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
271 // Initialize the adapter
272 SummarySearchAdapter adapter = new SummarySearchAdapter(this);
273 mAdapter = new SectionAdapter<SearchResult, SummarySearchAdapter>(this, adapter);
275 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
276 mAdapter.setupHeaderParameters(R.layout.list_search_header, false);
277 mAdapter.setupFooterParameters(R.layout.list_search_footer, true);
278 mAdapter.setPopupMenuClickedListener(new IPopupMenuCallback.IListener() {
280 public void onPopupMenuClicked(View v, int position) {
281 mPopupMenuHelper.showPopupMenu(v, position);
285 mLoadingEmptyContainer = (LoadingEmptyContainer) findViewById(R.id.loading_empty_container);
286 // setup the no results container
287 NoResultsContainer noResults = mLoadingEmptyContainer.getNoResultsContainer();
288 noResults.setMainText(R.string.empty_search);
289 noResults.setSecondaryText(R.string.empty_search_check);
293 // setup handler and runnable
294 mHandler = new Handler();
295 mLoadingRunnable = new Runnable() {
298 setState(VisibleState.Loading);
302 // Theme the action bar
303 final ActionBar actionBar = getActionBar();
304 actionBar.setDisplayHomeAsUpEnabled(true);
306 // Get the query String
307 mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
309 // if we have a non-empty search string, this is a 2nd lvl search
310 if (!TextUtils.isEmpty(mFilterString)) {
311 mTopLevelSearch = false;
313 // get the search type to filter by
314 int type = getIntent().getIntExtra(SearchManager.SEARCH_MODE, -1);
315 if (type >= 0 && type < ResultType.values().length) {
316 mSearchType = ResultType.values()[type];
320 switch (mSearchType) {
322 resourceId = R.string.search_title_artists;
325 resourceId = R.string.search_title_albums;
328 resourceId = R.string.search_title_playlists;
331 resourceId = R.string.search_title_songs;
334 actionBar.setTitle(getString(resourceId, mFilterString).toUpperCase());
335 actionBar.setDisplayHomeAsUpEnabled(true);
338 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
340 // Start the loader for the query
341 getSupportLoaderManager().initLoader(SEARCH_LOADER, null, this);
343 mTopLevelSearch = true;
344 mSearchHistoryCallback = new SearchHistoryCallback();
346 // Start the loader for the search history
347 getSupportLoaderManager().initLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
350 // set the background on the root view
351 getWindow().getDecorView().getRootView().setBackgroundColor(
352 getResources().getColor(R.color.background_color));
356 * Sets up the list view
358 private void initListView() {
359 // Initialize the grid
360 mListView = (ListView)findViewById(R.id.list_base);
361 // Set the data behind the list
362 mListView.setAdapter(mAdapter);
363 // Release any references to the recycled Views
364 mListView.setRecyclerListener(new RecycleHolder());
365 // Show the albums and songs from the selected artist
366 mListView.setOnItemClickListener(this);
367 // To help make scrolling smooth
368 mListView.setOnScrollListener(this);
369 // sets the touch listener
370 mListView.setOnTouchListener(this);
371 // If we setEmptyView with mLoadingEmptyContainer it causes a crash in DragSortListView
372 // when updating the search. For now let's manually toggle visibility and come back
374 //mListView.setEmptyView(mLoadingEmptyContainer);
376 // load the search history list view
377 mSearchHistoryListView = (ListView)findViewById(R.id.list_search_history);
378 mSearchHistoryListView.setOnItemClickListener(new OnItemClickListener() {
380 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
381 String searchItem = (String)mSearchHistoryListView.getAdapter().getItem(position);
382 mSearchView.setQuery(searchItem, true);
385 mSearchHistoryListView.setOnTouchListener(this);
392 public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id,
394 IItemCompare<SearchResult> comparator = null;
396 // prep the loader in case the query takes a long time
399 // if we are at the top level, create a comparator to separate the different types into
400 // their own sections (artists, albums, etc)
401 if (mTopLevelSearch) {
402 comparator = SectionCreatorUtils.createSearchResultComparison(this);
405 return new SectionCreator<SearchResult>(this,
406 new SummarySearchLoader(this, mFilterString, mSearchType),
414 public boolean onCreateOptionsMenu(final Menu menu) {
415 // if we are not a top level search view, we do not need to create the search fields
416 if (!mTopLevelSearch) {
417 return super.onCreateOptionsMenu(menu);
421 getMenuInflater().inflate(R.menu.search, menu);
423 // Filter the list the user is looking it via SearchView
424 MenuItem searchItem = menu.findItem(R.id.menu_search);
425 mSearchView = (SearchView)searchItem.getActionView();
426 mSearchView.setOnQueryTextListener(this);
427 mSearchView.setQueryHint(getString(R.string.searchHint).toUpperCase());
429 // The SearchView has no way for you to customize or get access to the search icon in a
430 // normal fashion, so we need to manually look for the icon and change the
431 // layout params to hide it
432 mSearchView.setIconifiedByDefault(false);
433 mSearchView.setIconified(false);
434 int searchButtonId = getResources().getIdentifier("android:id/search_mag_icon", null, null);
435 ImageView searchIcon = (ImageView)mSearchView.findViewById(searchButtonId);
436 searchIcon.setLayoutParams(new LinearLayout.LayoutParams(0, 0));
438 searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
440 public boolean onMenuItemActionExpand(MenuItem item) {
445 public boolean onMenuItemActionCollapse(MenuItem item) {
451 menu.findItem(R.id.menu_search).expandActionView();
453 return super.onCreateOptionsMenu(menu);
456 private void quit() {
465 protected void onDestroy() {
467 // Unbind from the service
468 if (mService != null) {
469 MusicUtils.unbindFromService(mToken);
478 public boolean onOptionsItemSelected(final MenuItem item) {
479 switch (item.getItemId()) {
480 case android.R.id.home:
486 return super.onOptionsItemSelected(item);
493 public void onLoadFinished(final Loader<SectionListContainer<SearchResult>> loader,
494 final SectionListContainer<SearchResult> data) {
495 // Check for any errors
496 if (data.mListResults.isEmpty()) {
499 // show the empty state
500 setState(VisibleState.Empty);
503 mAdapter.setData(data);
504 // show the search results
505 setState(VisibleState.SearchResults);
513 public void onLoaderReset(final Loader<SectionListContainer<SearchResult>> loader) {
521 public void onScrollStateChanged(final AbsListView view, final int scrollState) {
522 // Pause disk cache access to ensure smoother scrolling
523 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
524 mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
526 mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
527 mAdapter.notifyDataSetChanged();
535 public boolean onQueryTextSubmit(final String query) {
536 // simulate an on query text change
537 onQueryTextChange(query);
538 // hide the input manager
544 public void hideInputManager() {
545 // When the search is "committed" by the user, then hide the keyboard so
546 // the user can more easily browse the list of results.
547 if (mSearchView != null) {
549 mImm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
551 mSearchView.clearFocus();
553 // add our search string
554 SearchHistory.getInstance(this).addSearchString(mFilterString);
559 * This posts a delayed for showing the loading screen. The reason for the delayed is we
560 * don't want to flash the loading icon very often since searches usually are pretty fast
562 public void setLoading() {
563 if (mCurrentState != VisibleState.Loading) {
564 if (!mHandler.hasCallbacks(mLoadingRunnable)) {
565 mHandler.postDelayed(mLoadingRunnable, LOADING_DELAY);
571 * Sets the currently visible view
572 * @param state the current visible state
574 public void setState(VisibleState state) {
575 // remove any delayed runnables. This has to be before mCurrentState == state
576 // in case the state doesn't change but we've created a loading runnable
577 mHandler.removeCallbacks(mLoadingRunnable);
579 // if we are already looking at view already, just quit
580 if (mCurrentState == state) {
584 mCurrentState = state;
586 mSearchHistoryListView.setVisibility(View.INVISIBLE);
587 mListView.setVisibility(View.INVISIBLE);
588 mLoadingEmptyContainer.setVisibility(View.INVISIBLE);
590 switch (mCurrentState) {
592 mSearchHistoryListView.setVisibility(View.VISIBLE);
595 mListView.setVisibility(View.VISIBLE);
598 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
599 mLoadingEmptyContainer.showNoResults();
602 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
603 mLoadingEmptyContainer.showLoading();
612 public boolean onQueryTextChange(final String newText) {
617 if (TextUtils.isEmpty(newText)) {
618 if (!TextUtils.isEmpty(mFilterString)) {
620 getSupportLoaderManager().restartLoader(HISTORY_LOADER, null,
621 mSearchHistoryCallback);
622 getSupportLoaderManager().destroyLoader(SEARCH_LOADER);
628 // if the strings are the same, return
629 if (newText.equals(mFilterString)) {
633 // Called when the action bar search text has changed. Update
634 // the search filter, and restart the loader to do a new query
636 mFilterString = newText;
638 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
639 getSupportLoaderManager().restartLoader(SEARCH_LOADER, null, this);
640 getSupportLoaderManager().destroyLoader(HISTORY_LOADER);
648 public void onItemClick(final AdapterView<?> parent, final View view, final int position,
650 if (mAdapter.isSectionFooter(position)) {
651 // since a footer should be after a list item by definition, let's look up the type
652 // of the previous item
653 SearchResult item = mAdapter.getTItem(position - 1);
654 Intent intent = new Intent(this, SearchActivity.class);
655 intent.putExtra(SearchManager.QUERY, mFilterString);
656 intent.putExtra(SearchManager.SEARCH_MODE, item.mType.ordinal());
657 startActivity(intent);
659 SearchResult item = mAdapter.getTItem(position);
660 switch (item.mType) {
662 NavUtils.openArtistProfile(this, item.mArtist);
665 NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
668 NavUtils.openPlaylist(this, item.mId, item.mTitle);
671 // If it's a song, play it and leave
672 final long[] list = new long[]{
675 MusicUtils.playAll(this, list, 0, -1, Config.IdType.NA, false);
685 public void onServiceConnected(final ComponentName name, final IBinder service) {
686 mService = IElevenService.Stub.asInterface(service);
693 public void onServiceDisconnected(final ComponentName name) {
698 * This class loads a search result summary of items
700 private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
701 private final String mQuery;
702 private final ResultType mSearchType;
704 public SummarySearchLoader(final Context context, final String query,
705 final ResultType searchType) {
708 mSearchType = searchType;
712 * This creates a search result given the data at the cursor position
713 * @param cursor at the position for the item
714 * @param type the type of item to create
715 * @return the search result
717 protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
718 SearchResult item = null;
722 item = SearchResult.createPlaylistResult(cursor);
723 item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
726 item = SearchResult.createSearchResult(cursor);
728 AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(),
730 if (details != null) {
731 item.mArtist = details.mArtistName;
732 item.mAlbum = details.mAlbumName;
733 item.mAlbumId = details.mAlbumId;
740 item = SearchResult.createSearchResult(cursor);
748 public List<SearchResult> loadInBackground() {
749 // if we are doing a specific type search, run that one
750 if (mSearchType != null && mSearchType != ResultType.Unknown) {
751 return runSearchForType();
754 return runGenericSearch();
758 * This creates a search for a specific type given a filter string. This will return the
759 * full list of results that matches those two requirements
760 * @return the results for that search
762 protected List<SearchResult> runSearchForType() {
763 ArrayList<SearchResult> results = new ArrayList<SearchResult>();
764 Cursor cursor = null;
766 if (mSearchType == ResultType.Playlist) {
767 cursor = makePlaylistSearchCursor(getContext(), mQuery);
769 cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
772 // pre-cache this index
773 final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
775 if (cursor != null && cursor.moveToFirst()) {
777 boolean addResult = true;
779 if (mSearchType != ResultType.Playlist) {
780 // get the result type
781 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
782 if (type != mSearchType) {
788 results.add(createSearchResult(cursor, mSearchType));
790 } while (cursor.moveToNext());
794 if (cursor != null) {
804 * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
806 * @return the results for that search
808 public List<SearchResult> runGenericSearch() {
809 ArrayList<SearchResult> results = new ArrayList<SearchResult>();
810 // number of types to query for
811 final int numTypes = ResultType.getNumTypes();
813 // number of results we want
814 final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;
816 // current number of results we have
817 int numResultsAdded = 0;
819 // count for each result type
820 int[] numOfEachType = new int[numTypes];
822 // search playlists first
823 Cursor playlistCursor = makePlaylistSearchCursor(getContext(), mQuery);
824 if (playlistCursor != null && playlistCursor.moveToFirst()) {
827 SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
831 } while (playlistCursor.moveToNext()
832 && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);
834 // because we deal with playlists separately,
835 // just mark that we have the full # of playlists
836 // so that logic later can quit out early if full
837 numResultsAdded = Config.SEARCH_NUM_RESULTS_TO_GET;
840 playlistCursor.close();
841 playlistCursor = null;
844 // do fancy audio search
845 Cursor cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
847 // pre-cache this index
848 final int mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
850 // walk through the cursor
851 if (cursor != null && cursor.moveToFirst()) {
853 // get the result type
854 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
856 // if we still need this type
857 if (numOfEachType[type.ordinal()] < Config.SEARCH_NUM_RESULTS_TO_GET) {
858 // get the search result
859 SearchResult item = createSearchResult(cursor, type);
864 numOfEachType[type.ordinal()]++;
867 // if we have enough then quit
868 if (numResultsAdded >= numResultsNeeded) {
873 } while (cursor.moveToNext());
880 Collections.sort(results, SearchResult.COMPARATOR);
885 public static Cursor makePlaylistSearchCursor(final Context context,
886 final String searchTerms) {
887 if (TextUtils.isEmpty(searchTerms)) {
891 // trim out special characters like % or \ as well as things like "a" "and" etc
892 String trimmedSearchTerms = MusicUtils.getTrimmedName(searchTerms);
894 if (TextUtils.isEmpty(trimmedSearchTerms)) {
898 String[] keywords = trimmedSearchTerms.split(" ");
900 // prep the keyword for like search
901 for (int i = 0; i < keywords.length; i++) {
902 keywords[i] = "%" + keywords[i] + "%";
907 // make the where clause
908 for (int i = 0; i < keywords.length; i++) {
910 where = "name LIKE ?";
912 where += " AND name LIKE ?";
916 return context.getContentResolver().query(
917 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
922 MediaStore.Audio.PlaylistsColumns.NAME
923 }, where, keywords, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
928 * Loads the search history in the background and creates an array adapter
930 public static class SearchHistoryLoader extends WrappedAsyncTaskLoader<ArrayAdapter<String>> {
931 public SearchHistoryLoader(Context context) {
936 public ArrayAdapter<String> loadInBackground() {
937 ArrayList<String> strings = SearchHistory.getInstance(getContext()).getRecentSearches();
938 ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(),
939 R.layout.list_item_search_history, R.id.line_one);
940 adapter.addAll(strings);
946 * This handles the Loader callbacks for the search history
948 public class SearchHistoryCallback implements LoaderCallbacks<ArrayAdapter<String>> {
950 public Loader<ArrayAdapter<String>> onCreateLoader(int i, Bundle bundle) {
951 // prep the loader in case the query takes a long time
954 return new SearchHistoryLoader(SearchActivity.this);
958 public void onLoadFinished(Loader<ArrayAdapter<String>> searchHistoryAdapterLoader,
959 ArrayAdapter<String> searchHistoryAdapter) {
960 // show the search history
961 setState(VisibleState.SearchHistory);
963 mSearchHistoryListView.setAdapter(searchHistoryAdapter);
967 public void onLoaderReset(Loader<ArrayAdapter<String>> cursorAdapterLoader) {
968 ((ArrayAdapter)mSearchHistoryListView.getAdapter()).clear();
976 public void onScroll(final AbsListView view, final int firstVisibleItem,
977 final int visibleItemCount, final int totalItemCount) {
982 public boolean onTouch(View v, MotionEvent event) {