2 * Copyright (C) 2012 Andrew Neal Licensed under the Apache License, Version 2.0
3 * (the "License"); you may not use this file except in compliance with the
4 * License. You may obtain a copy of the License at
5 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
12 package com.cyngn.eleven.ui.activities;
14 import android.app.ActionBar;
15 import android.app.SearchManager;
16 import android.content.ComponentName;
17 import android.content.Context;
18 import android.content.Intent;
19 import android.content.ServiceConnection;
20 import android.database.Cursor;
21 import android.media.AudioManager;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.os.IBinder;
25 import android.provider.BaseColumns;
26 import android.provider.MediaStore;
27 import android.support.v4.app.FragmentActivity;
28 import android.support.v4.app.LoaderManager.LoaderCallbacks;
29 import android.support.v4.content.Loader;
30 import android.text.TextUtils;
31 import android.view.Menu;
32 import android.view.MenuItem;
33 import android.view.MotionEvent;
34 import android.view.View;
35 import android.view.inputmethod.InputMethodManager;
36 import android.widget.AbsListView;
37 import android.widget.AbsListView.OnScrollListener;
38 import android.widget.AdapterView;
39 import android.widget.AdapterView.OnItemClickListener;
40 import android.widget.ArrayAdapter;
41 import android.widget.ImageView;
42 import android.widget.LinearLayout;
43 import android.widget.ListView;
44 import android.widget.SearchView;
45 import android.widget.SearchView.OnQueryTextListener;
47 import com.cyngn.eleven.Config;
48 import com.cyngn.eleven.IElevenService;
49 import com.cyngn.eleven.R;
50 import com.cyngn.eleven.adapters.SummarySearchAdapter;
51 import com.cyngn.eleven.loaders.WrappedAsyncTaskLoader;
52 import com.cyngn.eleven.menu.FragmentMenuItems;
53 import com.cyngn.eleven.model.AlbumArtistDetails;
54 import com.cyngn.eleven.model.SearchResult;
55 import com.cyngn.eleven.model.SearchResult.ResultType;
56 import com.cyngn.eleven.provider.SearchHistory;
57 import com.cyngn.eleven.recycler.RecycleHolder;
58 import com.cyngn.eleven.sectionadapter.SectionAdapter;
59 import com.cyngn.eleven.sectionadapter.SectionCreator;
60 import com.cyngn.eleven.sectionadapter.SectionCreator.SimpleListLoader;
61 import com.cyngn.eleven.sectionadapter.SectionListContainer;
62 import com.cyngn.eleven.utils.ApolloUtils;
63 import com.cyngn.eleven.utils.MusicUtils;
64 import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
65 import com.cyngn.eleven.utils.NavUtils;
66 import com.cyngn.eleven.utils.PopupMenuHelper;
67 import com.cyngn.eleven.utils.SectionCreatorUtils;
68 import com.cyngn.eleven.utils.SectionCreatorUtils.IItemCompare;
69 import com.cyngn.eleven.widgets.IPopupMenuCallback;
70 import com.cyngn.eleven.widgets.NoResultsContainer;
72 import java.util.ArrayList;
73 import java.util.Collections;
74 import java.util.List;
75 import java.util.TreeSet;
77 import static android.view.View.OnTouchListener;
78 import static com.cyngn.eleven.utils.MusicUtils.mService;
81 * Provides the search interface for Apollo.
83 * @author Andrew Neal (andrewdneal@gmail.com)
85 public class SearchActivity extends FragmentActivity implements
86 LoaderCallbacks<SectionListContainer<SearchResult>>,
87 OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection,
90 * Loading delay of 500ms so we don't flash the screen too much when loading new searches
92 private static int LOADING_DELAY = 500;
95 * Identifier for the search loader
97 private static int SEARCH_LOADER = 0;
100 * Identifier for the search history loader
102 private static int HISTORY_LOADER = 1;
107 private ServiceToken mToken;
112 private String mFilterString;
117 private ListView mListView;
120 * Used the filter the user's music
122 private SearchView mSearchView;
127 private InputMethodManager mImm;
130 * The view that container the no search results text
132 private NoResultsContainer mNoResultsContainer;
137 private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;
140 * boolean tracking whether this is the search level when the user first enters search
141 * or if the user has clicked show all
143 private boolean mTopLevelSearch;
146 * If the user has clicked show all, this tells us what type (Artist, Album, etc)
148 private ResultType mSearchType;
151 * Search History loader callback
153 private SearchHistoryCallback mSearchHistoryCallback;
158 private ListView mSearchHistoryListView;
161 * This tracks our current visible state between the different views
170 private VisibleState mCurrentState;
173 * Handler for posting runnables
175 private Handler mHandler;
178 * A runnable to show the loading view that will be posted with a delay to prevent flashing
180 private Runnable mLoadingRunnable;
183 * Flag used to track if we are quitting so we don't flash loaders while finishing the activity
185 private boolean mQuitting = false;
190 private PopupMenuHelper mPopupMenuHelper;
196 public void onCreate(final Bundle savedInstanceState) {
197 super.onCreate(savedInstanceState);
199 mPopupMenuHelper = new PopupMenuHelper(this, getSupportFragmentManager()) {
200 private SearchResult mSelectedItem;
203 protected PopupMenuType onPreparePopupMenu(int position) {
204 mSelectedItem = mAdapter.getTItem(position);
206 return PopupMenuType.SearchResult;
210 protected long[] getIdList() {
211 switch (mSelectedItem.mType) {
213 return MusicUtils.getSongListForArtist(SearchActivity.this,
216 return MusicUtils.getSongListForAlbum(SearchActivity.this,
219 return new long[] { mSelectedItem.mId };
221 return MusicUtils.getSongListForPlaylist(SearchActivity.this,
229 protected void updateMenuIds(PopupMenuType type, TreeSet<Integer> set) {
230 super.updateMenuIds(type, set);
232 if (mSelectedItem.mType == ResultType.Album) {
233 set.add(FragmentMenuItems.MORE_BY_ARTIST);
238 protected String getArtistName() {
239 return mSelectedItem.mArtist;
244 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
246 // Control the media volume
247 setVolumeControlStream(AudioManager.STREAM_MUSIC);
249 // Bind Apollo's service
250 mToken = MusicUtils.bindToService(this, this);
253 setContentView(R.layout.activity_search);
255 // get the input method manager
256 mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
258 // Initialize the adapter
259 SummarySearchAdapter adapter = new SummarySearchAdapter(this);
260 mAdapter = new SectionAdapter<SearchResult, SummarySearchAdapter>(this, adapter);
262 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
263 mAdapter.setupHeaderParameters(R.layout.list_search_header, false);
264 mAdapter.setupFooterParameters(R.layout.list_search_footer, true);
265 mAdapter.setPopupMenuClickedListener(new IPopupMenuCallback.IListener() {
267 public void onPopupMenuClicked(View v, int position) {
268 mPopupMenuHelper.showPopupMenu(v, position);
272 // setup the no results container
273 mNoResultsContainer = (NoResultsContainer)findViewById(R.id.no_results_container);
274 mNoResultsContainer.setMainText(R.string.empty_search);
275 mNoResultsContainer.setSecondaryText(R.string.empty_search_check);
279 // setup handler and runnable
280 mHandler = new Handler();
281 mLoadingRunnable = new Runnable() {
284 setState(VisibleState.Loading);
288 // Theme the action bar
289 final ActionBar actionBar = getActionBar();
290 actionBar.setHomeButtonEnabled(true);
291 actionBar.setIcon(R.drawable.ic_action_search);
293 // Get the query String
294 mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
296 // if we have a non-empty search string, this is a 2nd lvl search
297 if (!TextUtils.isEmpty(mFilterString)) {
298 mTopLevelSearch = false;
300 // get the search type to filter by
301 int type = getIntent().getIntExtra(SearchManager.SEARCH_MODE, -1);
302 if (type >= 0 && type < ResultType.values().length) {
303 mSearchType = ResultType.values()[type];
307 switch (mSearchType) {
309 resourceId = R.string.search_title_artists;
312 resourceId = R.string.search_title_albums;
315 resourceId = R.string.search_title_playlists;
318 resourceId = R.string.search_title_songs;
321 actionBar.setTitle(getString(resourceId, mFilterString).toUpperCase());
322 actionBar.setDisplayHomeAsUpEnabled(true);
325 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
327 // Start the loader for the query
328 getSupportLoaderManager().initLoader(SEARCH_LOADER, null, this);
330 mTopLevelSearch = true;
331 mSearchHistoryCallback = new SearchHistoryCallback();
333 // Start the loader for the search history
334 getSupportLoaderManager().initLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
337 // set the background on the root view
338 getWindow().getDecorView().getRootView().setBackgroundColor(
339 getResources().getColor(R.color.background_color));
343 * Sets up the list view
345 private void initListView() {
346 // Initialize the grid
347 mListView = (ListView)findViewById(R.id.list_base);
348 // Set the data behind the list
349 mListView.setAdapter(mAdapter);
350 // Release any references to the recycled Views
351 mListView.setRecyclerListener(new RecycleHolder());
352 // Show the albums and songs from the selected artist
353 mListView.setOnItemClickListener(this);
354 // To help make scrolling smooth
355 mListView.setOnScrollListener(this);
356 // sets the touch listener
357 mListView.setOnTouchListener(this);
358 // If we setEmptyView with noResultsContainer it causes a crash in DragSortListView
359 // when updating the search. For now let's manually toggle visibility and come back
361 //mListView.setEmptyView(mNoResultsContainer);
363 // load the search history list view
364 mSearchHistoryListView = (ListView)findViewById(R.id.list_search_history);
365 mSearchHistoryListView.setOnItemClickListener(new OnItemClickListener() {
367 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
368 String searchItem = (String)mSearchHistoryListView.getAdapter().getItem(position);
369 mSearchView.setQuery(searchItem, true);
372 mSearchHistoryListView.setOnTouchListener(this);
379 public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id,
381 IItemCompare<SearchResult> comparator = null;
383 // prep the loader in case the query takes a long time
386 // set the no results string ahead of time in case the user changes the string whiel loading
387 mNoResultsContainer.setMainHighlightText("\"" + mFilterString + "\"");
389 // if we are at the top level, create a comparator to separate the different types into
390 // their own sections (artists, albums, etc)
391 if (mTopLevelSearch) {
392 comparator = SectionCreatorUtils.createSearchResultComparison(this);
395 return new SectionCreator<SearchResult>(this,
396 new SummarySearchLoader(this, mFilterString, mSearchType),
404 public boolean onCreateOptionsMenu(final Menu menu) {
405 // if we are not a top level search view, we do not need to create the search fields
406 if (!mTopLevelSearch) {
407 return super.onCreateOptionsMenu(menu);
411 getMenuInflater().inflate(R.menu.search, menu);
413 // Filter the list the user is looking it via SearchView
414 MenuItem searchItem = menu.findItem(R.id.menu_search);
415 mSearchView = (SearchView)searchItem.getActionView();
416 mSearchView.setOnQueryTextListener(this);
417 mSearchView.setQueryHint(getString(R.string.searchHint).toUpperCase());
419 // The SearchView has no way for you to customize or get access to the search icon in a
420 // normal fashion, so we need to manually look for the icon and change the
421 // layout params to hide it
422 mSearchView.setIconifiedByDefault(false);
423 mSearchView.setIconified(false);
424 int searchButtonId = getResources().getIdentifier("android:id/search_mag_icon", null, null);
425 ImageView searchIcon = (ImageView)mSearchView.findViewById(searchButtonId);
426 searchIcon.setLayoutParams(new LinearLayout.LayoutParams(0, 0));
428 searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
430 public boolean onMenuItemActionExpand(MenuItem item) {
435 public boolean onMenuItemActionCollapse(MenuItem item) {
441 menu.findItem(R.id.menu_search).expandActionView();
443 return super.onCreateOptionsMenu(menu);
446 private void quit() {
455 protected void onStart() {
457 MusicUtils.notifyForegroundStateChanged(this, true);
464 protected void onStop() {
466 MusicUtils.notifyForegroundStateChanged(this, false);
473 protected void onDestroy() {
475 // Unbind from the service
476 if (mService != null) {
477 MusicUtils.unbindFromService(mToken);
486 public boolean onOptionsItemSelected(final MenuItem item) {
487 switch (item.getItemId()) {
488 case android.R.id.home:
494 return super.onOptionsItemSelected(item);
501 public void onLoadFinished(final Loader<SectionListContainer<SearchResult>> loader,
502 final SectionListContainer<SearchResult> data) {
503 // Check for any errors
504 if (data.mListResults.isEmpty()) {
507 // show the empty state
508 setState(VisibleState.Empty);
511 mAdapter.setData(data);
512 // show the search results
513 setState(VisibleState.SearchResults);
521 public void onLoaderReset(final Loader<SectionListContainer<SearchResult>> loader) {
528 public void onScrollStateChanged(final AbsListView view, final int scrollState) {
529 // Pause disk cache access to ensure smoother scrolling
530 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
531 || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
532 mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
534 mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
535 mAdapter.notifyDataSetChanged();
543 public boolean onQueryTextSubmit(final String query) {
544 // simulate an on query text change
545 onQueryTextChange(query);
546 // hide the input manager
552 public void hideInputManager() {
553 // When the search is "committed" by the user, then hide the keyboard so
554 // the user can more easily browse the list of results.
555 if (mSearchView != null) {
557 mImm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
559 mSearchView.clearFocus();
561 // add our search string
562 SearchHistory.getInstance(this).addSearchString(mFilterString);
567 * This posts a delayed for showing the loading screen. The reason for the delayed is we
568 * don't want to flash the loading icon very often since searches usually are pretty fast
570 public void setLoading() {
571 if (mCurrentState != VisibleState.Loading) {
572 if (!mHandler.hasCallbacks(mLoadingRunnable)) {
573 mHandler.postDelayed(mLoadingRunnable, LOADING_DELAY);
579 * Sets the currently visible view
580 * @param state the current visible state
582 public void setState(VisibleState state) {
583 // remove any delayed runnables. This has to be before mCurrentState == state
584 // in case the state doesn't change but we've created a loading runnable
585 mHandler.removeCallbacks(mLoadingRunnable);
587 // if we are already looking at view already, just quit
588 if (mCurrentState == state) {
592 mCurrentState = state;
594 mSearchHistoryListView.setVisibility(View.INVISIBLE);
595 mListView.setVisibility(View.INVISIBLE);
596 mNoResultsContainer.setVisibility(View.INVISIBLE);
598 switch (mCurrentState) {
600 mSearchHistoryListView.setVisibility(View.VISIBLE);
603 mListView.setVisibility(View.VISIBLE);
606 mNoResultsContainer.setVisibility(View.VISIBLE);
609 // Don't show anything for now - we need a loading screen
610 // see bug: https://cyanogen.atlassian.net/browse/MUSIC-63
619 public boolean onQueryTextChange(final String newText) {
624 if (TextUtils.isEmpty(newText)) {
625 if (!TextUtils.isEmpty(mFilterString)) {
627 getSupportLoaderManager().restartLoader(HISTORY_LOADER, null,
628 mSearchHistoryCallback);
629 getSupportLoaderManager().destroyLoader(SEARCH_LOADER);
635 // if the strings are the same, return
636 if (newText.equals(mFilterString)) {
640 // Called when the action bar search text has changed. Update
641 // the search filter, and restart the loader to do a new query
643 mFilterString = newText;
645 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
646 getSupportLoaderManager().restartLoader(SEARCH_LOADER, null, this);
647 getSupportLoaderManager().destroyLoader(HISTORY_LOADER);
655 public void onItemClick(final AdapterView<?> parent, final View view, final int position,
657 if (mAdapter.isSectionFooter(position)) {
658 // since a footer should be after a list item by definition, let's look up the type
659 // of the previous item
660 SearchResult item = mAdapter.getTItem(position - 1);
661 Intent intent = new Intent(this, SearchActivity.class);
662 intent.putExtra(SearchManager.QUERY, mFilterString);
663 intent.putExtra(SearchManager.SEARCH_MODE, item.mType.ordinal());
664 startActivity(intent);
666 SearchResult item = mAdapter.getTItem(position);
667 switch (item.mType) {
669 NavUtils.openArtistProfile(this, item.mArtist);
672 NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
675 NavUtils.openPlaylist(this, item.mId, item.mTitle);
678 // If it's a song, play it and leave
679 final long[] list = new long[]{
682 MusicUtils.playAll(this, list, 0, false);
692 public void onServiceConnected(final ComponentName name, final IBinder service) {
693 mService = IElevenService.Stub.asInterface(service);
700 public void onServiceDisconnected(final ComponentName name) {
705 * This class loads a search result summary of items
707 private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
708 private final String mQuery;
709 private final ResultType mSearchType;
711 public SummarySearchLoader(final Context context, final String query,
712 final ResultType searchType) {
715 mSearchType = searchType;
719 * This creates a search result given the data at the cursor position
720 * @param cursor at the position for the item
721 * @param type the type of item to create
722 * @return the search result
724 protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
725 SearchResult item = null;
729 item = SearchResult.createPlaylistResult(cursor);
730 item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
733 item = SearchResult.createSearchResult(cursor);
735 AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(),
737 if (details != null) {
738 item.mArtist = details.mArtistName;
739 item.mAlbum = details.mAlbumName;
740 item.mAlbumId = details.mAlbumId;
747 item = SearchResult.createSearchResult(cursor);
755 public List<SearchResult> loadInBackground() {
756 // if we are doing a specific type search, run that one
757 if (mSearchType != null && mSearchType != ResultType.Unknown) {
758 return runSearchForType();
761 return runGenericSearch();
765 * This creates a search for a specific type given a filter string. This will return the
766 * full list of results that matches those two requirements
767 * @return the results for that search
769 protected List<SearchResult> runSearchForType() {
770 ArrayList<SearchResult> results = new ArrayList<SearchResult>();
771 Cursor cursor = null;
773 if (mSearchType == ResultType.Playlist) {
774 cursor = makePlaylistSearchCursor(getContext(), mQuery);
776 cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
779 // pre-cache this index
780 final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
782 if (cursor != null && cursor.moveToFirst()) {
784 boolean addResult = true;
786 if (mSearchType != ResultType.Playlist) {
787 // get the result type
788 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
789 if (type != mSearchType) {
795 results.add(createSearchResult(cursor, mSearchType));
797 } while (cursor.moveToNext());
801 if (cursor != null) {
811 * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
813 * @return the results for that search
815 public List<SearchResult> runGenericSearch() {
816 ArrayList<SearchResult> results = new ArrayList<SearchResult>();
817 // number of types to query for
818 final int numTypes = ResultType.getNumTypes();
820 // number of results we want
821 final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;
823 // current number of results we have
824 int numResultsAdded = 0;
826 // count for each result type
827 int[] numOfEachType = new int[numTypes];
829 // search playlists first
830 Cursor playlistCursor = makePlaylistSearchCursor(getContext(), mQuery);
831 if (playlistCursor != null && playlistCursor.moveToFirst()) {
834 SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
838 } while (playlistCursor.moveToNext()
839 && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);
841 // because we deal with playlists separately,
842 // just mark that we have the full # of playlists
843 // so that logic later can quit out early if full
844 numResultsAdded = Config.SEARCH_NUM_RESULTS_TO_GET;
847 playlistCursor.close();
848 playlistCursor = null;
851 // do fancy audio search
852 Cursor cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
854 // pre-cache this index
855 final int mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
857 // walk through the cursor
858 if (cursor != null && cursor.moveToFirst()) {
860 // get the result type
861 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
863 // if we still need this type
864 if (numOfEachType[type.ordinal()] < Config.SEARCH_NUM_RESULTS_TO_GET) {
865 // get the search result
866 SearchResult item = createSearchResult(cursor, type);
871 numOfEachType[type.ordinal()]++;
874 // if we have enough then quit
875 if (numResultsAdded >= numResultsNeeded) {
880 } while (cursor.moveToNext());
887 Collections.sort(results, SearchResult.COMPARATOR);
892 public static Cursor makePlaylistSearchCursor(final Context context,
893 final String searchTerms) {
894 if (TextUtils.isEmpty(searchTerms)) {
898 // trim out special characters like % or \ as well as things like "a" "and" etc
899 String trimmedSearchTerms = MusicUtils.getTrimmedName(searchTerms);
901 if (TextUtils.isEmpty(trimmedSearchTerms)) {
905 String[] keywords = trimmedSearchTerms.split(" ");
907 // prep the keyword for like search
908 for (int i = 0; i < keywords.length; i++) {
909 keywords[i] = "%" + keywords[i] + "%";
914 // make the where clause
915 for (int i = 0; i < keywords.length; i++) {
917 where = "name LIKE ?";
919 where += " AND name LIKE ?";
923 return context.getContentResolver().query(
924 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
929 MediaStore.Audio.PlaylistsColumns.NAME
930 }, where, keywords, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
935 * Loads the search history in the background and creates an array adapter
937 public static class SearchHistoryLoader extends WrappedAsyncTaskLoader<ArrayAdapter<String>> {
938 public SearchHistoryLoader(Context context) {
943 public ArrayAdapter<String> loadInBackground() {
944 ArrayList<String> strings = SearchHistory.getInstance(getContext()).getRecentSearches();
945 ArrayAdapter<String> adapter = new ArrayAdapter<String>(getContext(),
946 R.layout.list_item_search_history, R.id.line_one);
947 adapter.addAll(strings);
953 * This handles the Loader callbacks for the search history
955 public class SearchHistoryCallback implements LoaderCallbacks<ArrayAdapter<String>> {
957 public Loader<ArrayAdapter<String>> onCreateLoader(int i, Bundle bundle) {
958 // prep the loader in case the query takes a long time
961 return new SearchHistoryLoader(SearchActivity.this);
965 public void onLoadFinished(Loader<ArrayAdapter<String>> searchHistoryAdapterLoader,
966 ArrayAdapter<String> searchHistoryAdapter) {
967 // show the search history
968 setState(VisibleState.SearchHistory);
970 mSearchHistoryListView.setAdapter(searchHistoryAdapter);
974 public void onLoaderReset(Loader<ArrayAdapter<String>> cursorAdapterLoader) {
983 public void onScroll(final AbsListView view, final int firstVisibleItem,
984 final int visibleItemCount, final int totalItemCount) {
989 public boolean onTouch(View v, MotionEvent event) {