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 org.lineageos.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.text.TextUtils;
30 import android.view.Menu;
31 import android.view.MenuItem;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.inputmethod.InputMethodManager;
35 import android.widget.AbsListView;
36 import android.widget.AbsListView.OnScrollListener;
37 import android.widget.AdapterView;
38 import android.widget.AdapterView.OnItemClickListener;
39 import android.widget.ArrayAdapter;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.ListView;
43 import android.widget.SearchView;
44 import android.widget.SearchView.OnQueryTextListener;
46 import androidx.fragment.app.FragmentActivity;
47 import androidx.loader.app.LoaderManager;
48 import androidx.loader.content.Loader;
50 import org.lineageos.eleven.Config;
51 import org.lineageos.eleven.IElevenService;
52 import org.lineageos.eleven.R;
53 import org.lineageos.eleven.adapters.SummarySearchAdapter;
54 import org.lineageos.eleven.loaders.WrappedAsyncTaskLoader;
55 import org.lineageos.eleven.menu.FragmentMenuItems;
56 import org.lineageos.eleven.model.AlbumArtistDetails;
57 import org.lineageos.eleven.model.SearchResult;
58 import org.lineageos.eleven.model.SearchResult.ResultType;
59 import org.lineageos.eleven.provider.SearchHistory;
60 import org.lineageos.eleven.recycler.RecycleHolder;
61 import org.lineageos.eleven.sectionadapter.SectionAdapter;
62 import org.lineageos.eleven.sectionadapter.SectionCreator;
63 import org.lineageos.eleven.sectionadapter.SectionCreator.SimpleListLoader;
64 import org.lineageos.eleven.sectionadapter.SectionListContainer;
65 import org.lineageos.eleven.utils.ElevenUtils;
66 import org.lineageos.eleven.utils.MusicUtils;
67 import org.lineageos.eleven.utils.MusicUtils.ServiceToken;
68 import org.lineageos.eleven.utils.NavUtils;
69 import org.lineageos.eleven.utils.PopupMenuHelper;
70 import org.lineageos.eleven.utils.SectionCreatorUtils;
71 import org.lineageos.eleven.utils.SectionCreatorUtils.IItemCompare;
72 import org.lineageos.eleven.widgets.IPopupMenuCallback;
73 import org.lineageos.eleven.widgets.LoadingEmptyContainer;
74 import org.lineageos.eleven.widgets.NoResultsContainer;
76 import java.util.ArrayList;
77 import java.util.Collections;
78 import java.util.List;
79 import java.util.TreeSet;
81 import static android.view.View.OnTouchListener;
82 import static org.lineageos.eleven.utils.MusicUtils.mService;
85 * Provides the search interface for Eleven.
87 * @author Andrew Neal (andrewdneal@gmail.com)
89 public class SearchActivity extends FragmentActivity implements
90 LoaderManager.LoaderCallbacks<SectionListContainer<SearchResult>>,
91 OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection,
94 * Intent extra for identifying the search type to filter for
96 public static String EXTRA_SEARCH_MODE = "search_mode";
99 * Loading delay of 500ms so we don't flash the screen too much when loading new searches
101 private static int LOADING_DELAY = 500;
104 * Identifier for the search loader
106 private static int SEARCH_LOADER = 0;
109 * Identifier for the search history loader
111 private static int HISTORY_LOADER = 1;
116 private ServiceToken mToken;
121 private String mFilterString;
126 private ListView mListView;
129 * Used the filter the user's music
131 private SearchView mSearchView;
136 private InputMethodManager mImm;
139 * The view that container the no search results text and the loading progress bar
141 private LoadingEmptyContainer mLoadingEmptyContainer;
146 private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;
149 * boolean tracking whether this is the search level when the user first enters search
150 * or if the user has clicked show all
152 private boolean mTopLevelSearch;
155 * If the user has clicked show all, this tells us what type (Artist, Album, etc)
157 private ResultType mSearchType;
160 * Search History loader callback
162 private SearchHistoryCallback mSearchHistoryCallback;
167 private ListView mSearchHistoryListView;
170 * This tracks our current visible state between the different views
179 private VisibleState mCurrentState;
182 * Handler for posting runnables
184 private Handler mHandler;
187 * A runnable to show the loading view that will be posted with a delay to prevent flashing
189 private Runnable mLoadingRunnable;
192 * Flag used to track if we are quitting so we don't flash loaders while finishing the activity
194 private boolean mQuitting = false;
199 private PopupMenuHelper mPopupMenuHelper;
205 public void onCreate(final Bundle savedInstanceState) {
206 super.onCreate(savedInstanceState);
208 mPopupMenuHelper = new PopupMenuHelper(this, getSupportFragmentManager()) {
209 private SearchResult mSelectedItem;
212 public PopupMenuType onPreparePopupMenu(int position) {
213 mSelectedItem = mAdapter.getTItem(position);
215 return PopupMenuType.SearchResult;
219 protected long[] getIdList() {
220 switch (mSelectedItem.mType) {
222 return MusicUtils.getSongListForArtist(SearchActivity.this,
225 return MusicUtils.getSongListForAlbum(SearchActivity.this,
228 return new long[] { mSelectedItem.mId };
230 return MusicUtils.getSongListForPlaylist(SearchActivity.this,
238 protected long getSourceId() {
239 return mSelectedItem.mId;
243 protected Config.IdType getSourceType() {
244 return mSelectedItem.mType.getSourceType();
248 protected void updateMenuIds(PopupMenuType type, TreeSet<Integer> set) {
249 super.updateMenuIds(type, set);
251 if (mSelectedItem.mType == ResultType.Album) {
252 set.add(FragmentMenuItems.MORE_BY_ARTIST);
257 protected String getArtistName() {
258 return mSelectedItem.mArtist;
262 // Control the media volume
263 setVolumeControlStream(AudioManager.STREAM_MUSIC);
265 // Bind Eleven's service
266 mToken = MusicUtils.bindToService(this, this);
269 setContentView(R.layout.activity_search);
271 // get the input method manager
272 mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
274 // Initialize the adapter
275 SummarySearchAdapter adapter = new SummarySearchAdapter(this);
276 mAdapter = new SectionAdapter<>(this, adapter);
278 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
279 mAdapter.setupHeaderParameters(R.layout.list_search_header, false);
280 mAdapter.setupFooterParameters(R.layout.list_search_footer, true);
281 mAdapter.setPopupMenuClickedListener(new IPopupMenuCallback.IListener() {
283 public void onPopupMenuClicked(View v, int position) {
284 mPopupMenuHelper.showPopupMenu(v, position);
288 mLoadingEmptyContainer = (LoadingEmptyContainer) findViewById(R.id.loading_empty_container);
289 // setup the no results container
290 NoResultsContainer noResults = mLoadingEmptyContainer.getNoResultsContainer();
291 noResults.setMainText(R.string.empty_search);
292 noResults.setSecondaryText(R.string.empty_search_check);
296 // setup handler and runnable
297 mHandler = new Handler();
298 mLoadingRunnable = new Runnable() {
301 setState(VisibleState.Loading);
305 // Theme the action bar
306 final ActionBar actionBar = getActionBar();
307 actionBar.setDisplayHomeAsUpEnabled(true);
309 // Get the query String
310 mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
312 // if we have a non-empty search string, this is a 2nd lvl search
313 if (!TextUtils.isEmpty(mFilterString)) {
314 mTopLevelSearch = false;
316 // get the search type to filter by
317 int type = getIntent().getIntExtra(SearchActivity.EXTRA_SEARCH_MODE, -1);
318 if (type >= 0 && type < ResultType.values().length) {
319 mSearchType = ResultType.values()[type];
323 switch (mSearchType) {
325 resourceId = R.string.search_title_artists;
328 resourceId = R.string.search_title_albums;
331 resourceId = R.string.search_title_playlists;
334 resourceId = R.string.search_title_songs;
337 actionBar.setTitle(getString(resourceId, mFilterString));
338 actionBar.setDisplayHomeAsUpEnabled(true);
341 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
343 // Start the loader for the query
344 getSupportLoaderManager().initLoader(SEARCH_LOADER, null, this);
346 mTopLevelSearch = true;
347 mSearchHistoryCallback = new SearchHistoryCallback();
349 // Start the loader for the search history
350 getSupportLoaderManager().initLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
355 * Sets up the list view
357 private void initListView() {
358 // Initialize the grid
359 mListView = (ListView)findViewById(R.id.list_base);
360 // Set the data behind the list
361 mListView.setAdapter(mAdapter);
362 // Release any references to the recycled Views
363 mListView.setRecyclerListener(new RecycleHolder());
364 // Show the albums and songs from the selected artist
365 mListView.setOnItemClickListener(this);
366 // To help make scrolling smooth
367 mListView.setOnScrollListener(this);
368 // sets the touch listener
369 mListView.setOnTouchListener(this);
370 // If we setEmptyView with mLoadingEmptyContainer it causes a crash in DragSortListView
371 // when updating the search. For now let's manually toggle visibility and come back
373 //mListView.setEmptyView(mLoadingEmptyContainer);
375 // load the search history list view
376 mSearchHistoryListView = (ListView)findViewById(R.id.list_search_history);
377 mSearchHistoryListView.setOnItemClickListener(new OnItemClickListener() {
379 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
380 String searchItem = (String)mSearchHistoryListView.getAdapter().getItem(position);
381 mSearchView.setQuery(searchItem, true);
384 mSearchHistoryListView.setOnTouchListener(this);
391 public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id,
393 IItemCompare<SearchResult> comparator = null;
395 // prep the loader in case the query takes a long time
398 // if we are at the top level, create a comparator to separate the different types into
399 // their own sections (artists, albums, etc)
400 if (mTopLevelSearch) {
401 comparator = SectionCreatorUtils.createSearchResultComparison(this);
404 return new SectionCreator<>(this,
405 new SummarySearchLoader(this, mFilterString, mSearchType),
413 public boolean onCreateOptionsMenu(final Menu menu) {
414 // if we are not a top level search view, we do not need to create the search fields
415 if (!mTopLevelSearch) {
416 return super.onCreateOptionsMenu(menu);
420 getMenuInflater().inflate(R.menu.search, menu);
422 // Filter the list the user is looking it via SearchView
423 MenuItem searchItem = menu.findItem(R.id.menu_search);
424 mSearchView = (SearchView)searchItem.getActionView();
425 mSearchView.setOnQueryTextListener(this);
426 mSearchView.setQueryHint(getString(R.string.searchHint));
428 // The SearchView has no way for you to customize or get access to the search icon in a
429 // normal fashion, so we need to manually look for the icon and change the
430 // layout params to hide it
431 mSearchView.setIconifiedByDefault(false);
432 mSearchView.setIconified(false);
433 int searchButtonId = getResources().getIdentifier("android:id/search_mag_icon", null, null);
434 ImageView searchIcon = (ImageView)mSearchView.findViewById(searchButtonId);
435 searchIcon.setLayoutParams(new LinearLayout.LayoutParams(0, 0));
437 searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
439 public boolean onMenuItemActionExpand(MenuItem item) {
444 public boolean onMenuItemActionCollapse(MenuItem item) {
450 menu.findItem(R.id.menu_search).expandActionView();
452 return super.onCreateOptionsMenu(menu);
455 private void quit() {
464 protected void onDestroy() {
466 // Unbind from the service
467 if (mService != null) {
468 MusicUtils.unbindFromService(mToken);
477 public boolean onOptionsItemSelected(final MenuItem item) {
478 switch (item.getItemId()) {
479 case android.R.id.home:
485 return super.onOptionsItemSelected(item);
492 public void onLoadFinished(final Loader<SectionListContainer<SearchResult>> loader,
493 final SectionListContainer<SearchResult> data) {
494 // Check for any errors
495 if (data.mListResults.isEmpty()) {
498 // show the empty state
499 setState(VisibleState.Empty);
502 mAdapter.setData(data);
503 // show the search results
504 setState(VisibleState.SearchResults);
512 public void onLoaderReset(final Loader<SectionListContainer<SearchResult>> loader) {
520 public void onScrollStateChanged(final AbsListView view, final int scrollState) {
521 // Pause disk cache access to ensure smoother scrolling
522 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
523 mAdapter.getUnderlyingAdapter().setPauseDiskCache(true);
525 mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
526 mAdapter.notifyDataSetChanged();
534 public boolean onQueryTextSubmit(final String query) {
535 // simulate an on query text change
536 onQueryTextChange(query);
537 // hide the input manager
543 public void hideInputManager() {
544 // When the search is "committed" by the user, then hide the keyboard so
545 // the user can more easily browse the list of results.
546 if (mSearchView != null) {
548 mImm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
550 mSearchView.clearFocus();
552 // add our search string
553 SearchHistory.getInstance(this).addSearchString(mFilterString);
558 * This posts a delayed for showing the loading screen. The reason for the delayed is we
559 * don't want to flash the loading icon very often since searches usually are pretty fast
561 public void setLoading() {
562 if (mCurrentState != VisibleState.Loading) {
563 mHandler.removeCallbacks(mLoadingRunnable);
564 mHandler.postDelayed(mLoadingRunnable, LOADING_DELAY);
569 * Sets the currently visible view
570 * @param state the current visible state
572 public void setState(VisibleState state) {
573 // remove any delayed runnables. This has to be before mCurrentState == state
574 // in case the state doesn't change but we've created a loading runnable
575 mHandler.removeCallbacks(mLoadingRunnable);
577 // if we are already looking at view already, just quit
578 if (mCurrentState == state) {
582 mCurrentState = state;
584 mSearchHistoryListView.setVisibility(View.INVISIBLE);
585 mListView.setVisibility(View.INVISIBLE);
586 mLoadingEmptyContainer.setVisibility(View.INVISIBLE);
588 switch (mCurrentState) {
590 mSearchHistoryListView.setVisibility(View.VISIBLE);
593 mListView.setVisibility(View.VISIBLE);
596 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
597 mLoadingEmptyContainer.showNoResults();
600 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
601 mLoadingEmptyContainer.showLoading();
610 public boolean onQueryTextChange(final String newText) {
615 if (TextUtils.isEmpty(newText)) {
616 if (!TextUtils.isEmpty(mFilterString)) {
618 getSupportLoaderManager().restartLoader(HISTORY_LOADER, null,
619 mSearchHistoryCallback);
620 getSupportLoaderManager().destroyLoader(SEARCH_LOADER);
626 // if the strings are the same, return
627 if (newText.equals(mFilterString)) {
631 // Called when the action bar search text has changed. Update
632 // the search filter, and restart the loader to do a new query
634 mFilterString = newText;
636 mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
637 getSupportLoaderManager().restartLoader(SEARCH_LOADER, null, this);
638 getSupportLoaderManager().destroyLoader(HISTORY_LOADER);
646 public void onItemClick(final AdapterView<?> parent, final View view, final int position,
648 if (mAdapter.isSectionFooter(position)) {
649 // since a footer should be after a list item by definition, let's look up the type
650 // of the previous item
651 SearchResult item = mAdapter.getTItem(position - 1);
652 Intent intent = new Intent(this, SearchActivity.class);
653 intent.putExtra(SearchManager.QUERY, mFilterString);
654 intent.putExtra(SearchActivity.EXTRA_SEARCH_MODE, item.mType.ordinal());
655 startActivity(intent);
657 SearchResult item = mAdapter.getTItem(position);
658 switch (item.mType) {
660 NavUtils.openArtistProfile(this, item.mArtist);
663 NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
666 NavUtils.openPlaylist(this, item.mId, item.mTitle);
669 // If it's a song, play it and leave
670 final long[] list = new long[]{
673 MusicUtils.playAll(this, list, 0, -1, Config.IdType.NA, false);
683 public void onServiceConnected(final ComponentName name, final IBinder service) {
684 mService = IElevenService.Stub.asInterface(service);
691 public void onServiceDisconnected(final ComponentName name) {
696 * This class loads a search result summary of items
698 private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
699 private final String mQuery;
700 private final ResultType mSearchType;
702 public SummarySearchLoader(final Context context, final String query,
703 final ResultType searchType) {
706 mSearchType = searchType;
710 * This creates a search result given the data at the cursor position
711 * @param cursor at the position for the item
712 * @param type the type of item to create
713 * @return the search result
715 protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
716 SearchResult item = null;
720 item = SearchResult.createPlaylistResult(cursor);
721 item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
724 item = SearchResult.createSearchResult(cursor);
726 AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(),
728 if (details != null) {
729 item.mArtist = details.mArtistName;
730 item.mAlbum = details.mAlbumName;
731 item.mAlbumId = details.mAlbumId;
738 item = SearchResult.createSearchResult(cursor);
746 public List<SearchResult> loadInBackground() {
747 // if we are doing a specific type search, run that one
748 if (mSearchType != null && mSearchType != ResultType.Unknown) {
749 return runSearchForType();
752 return runGenericSearch();
756 * This creates a search for a specific type given a filter string. This will return the
757 * full list of results that matches those two requirements
758 * @return the results for that search
760 protected List<SearchResult> runSearchForType() {
761 ArrayList<SearchResult> results = new ArrayList<>();
762 Cursor cursor = null;
764 if (mSearchType == ResultType.Playlist) {
765 cursor = makePlaylistSearchCursor(getContext(), mQuery);
767 cursor = ElevenUtils.createSearchQueryCursor(getContext(), mQuery);
770 // pre-cache this index
771 final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
773 if (cursor != null && cursor.moveToFirst()) {
775 boolean addResult = true;
777 if (mSearchType != ResultType.Playlist) {
778 // get the result type
779 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
780 if (type != mSearchType) {
786 results.add(createSearchResult(cursor, mSearchType));
788 } while (cursor.moveToNext());
792 if (cursor != null) {
802 * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
804 * @return the results for that search
806 public List<SearchResult> runGenericSearch() {
807 ArrayList<SearchResult> results = new ArrayList<>();
808 // number of types to query for
809 final int numTypes = ResultType.getNumTypes();
811 // number of results we want
812 final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;
814 // current number of results we have
815 int numResultsAdded = 0;
817 // count for each result type
818 int[] numOfEachType = new int[numTypes];
820 // search playlists first
821 Cursor playlistCursor = makePlaylistSearchCursor(getContext(), mQuery);
822 if (playlistCursor != null && playlistCursor.moveToFirst()) {
825 SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
829 } while (playlistCursor.moveToNext()
830 && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);
832 // because we deal with playlists separately,
833 // just mark that we have the full # of playlists
834 // so that logic later can quit out early if full
835 numResultsAdded = Config.SEARCH_NUM_RESULTS_TO_GET;
838 playlistCursor.close();
839 playlistCursor = null;
842 // do fancy audio search
843 Cursor cursor = ElevenUtils.createSearchQueryCursor(getContext(), mQuery);
845 // pre-cache this index
846 final int mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
848 // walk through the cursor
849 if (cursor != null && cursor.moveToFirst()) {
851 // get the result type
852 ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
854 // if we still need this type
855 if (numOfEachType[type.ordinal()] < Config.SEARCH_NUM_RESULTS_TO_GET) {
856 // get the search result
857 SearchResult item = createSearchResult(cursor, type);
862 numOfEachType[type.ordinal()]++;
865 // if we have enough then quit
866 if (numResultsAdded >= numResultsNeeded) {
871 } while (cursor.moveToNext());
878 Collections.sort(results, SearchResult.COMPARATOR);
883 public static Cursor makePlaylistSearchCursor(final Context context,
884 final String searchTerms) {
885 if (TextUtils.isEmpty(searchTerms)) {
889 // trim out special characters like % or \ as well as things like "a" "and" etc
890 String trimmedSearchTerms = MusicUtils.getTrimmedName(searchTerms);
892 if (TextUtils.isEmpty(trimmedSearchTerms)) {
896 String[] keywords = trimmedSearchTerms.split(" ");
898 // prep the keyword for like search
899 for (int i = 0; i < keywords.length; i++) {
900 keywords[i] = "%" + keywords[i] + "%";
903 final StringBuilder where = new StringBuilder();
904 for (int i = 0; i < keywords.length; i++) {
906 where.append("name LIKE ?");
908 where.append(" AND name LIKE ?");
912 return context.getContentResolver().query(
913 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
918 MediaStore.Audio.PlaylistsColumns.NAME
919 }, where.toString(), keywords, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
924 * Loads the search history in the background and creates an array adapter
926 public static class SearchHistoryLoader extends WrappedAsyncTaskLoader<ArrayAdapter<String>> {
927 public SearchHistoryLoader(Context context) {
932 public ArrayAdapter<String> loadInBackground() {
933 ArrayList<String> strings = SearchHistory.getInstance(getContext()).getRecentSearches();
934 ArrayAdapter<String> adapter = new ArrayAdapter<>(getContext(),
935 R.layout.list_item_search_history, R.id.line_one);
936 adapter.addAll(strings);
942 * This handles the Loader callbacks for the search history
944 public class SearchHistoryCallback implements LoaderManager.LoaderCallbacks<ArrayAdapter<String>> {
946 public Loader<ArrayAdapter<String>> onCreateLoader(int i, Bundle bundle) {
947 // prep the loader in case the query takes a long time
950 return new SearchHistoryLoader(SearchActivity.this);
954 public void onLoadFinished(Loader<ArrayAdapter<String>> searchHistoryAdapterLoader,
955 ArrayAdapter<String> searchHistoryAdapter) {
956 // show the search history
957 setState(VisibleState.SearchHistory);
959 mSearchHistoryListView.setAdapter(searchHistoryAdapter);
963 public void onLoaderReset(Loader<ArrayAdapter<String>> cursorAdapterLoader) {
964 ((ArrayAdapter)mSearchHistoryListView.getAdapter()).clear();
972 public void onScroll(final AbsListView view, final int firstVisibleItem,
973 final int visibleItemCount, final int totalItemCount) {
978 public boolean onTouch(View v, MotionEvent event) {