OSDN Git Service

27ef14fbaef42602907b7c32ec15e9d2bc60a7b1
[android-x86/packages-apps-Eleven.git] / src / com / cyanogenmod / eleven / ui / activities / SearchActivity.java
1 /*
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.
12  */
13
14 package com.cyanogenmod.eleven.ui.activities;
15
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;
48
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;
74
75 import java.util.ArrayList;
76 import java.util.Collections;
77 import java.util.List;
78 import java.util.TreeSet;
79
80 import static android.view.View.OnTouchListener;
81 import static com.cyanogenmod.eleven.utils.MusicUtils.mService;
82
83 /**
84  * Provides the search interface for Apollo.
85  * 
86  * @author Andrew Neal (andrewdneal@gmail.com)
87  */
88 public class SearchActivity extends FragmentActivity implements
89         LoaderCallbacks<SectionListContainer<SearchResult>>,
90         OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection,
91         OnTouchListener {
92     /**
93      * Loading delay of 500ms so we don't flash the screen too much when loading new searches
94      */
95     private static int LOADING_DELAY = 500;
96
97     /**
98      * Identifier for the search loader
99      */
100     private static int SEARCH_LOADER = 0;
101
102     /**
103      * Identifier for the search history loader
104      */
105     private static int HISTORY_LOADER = 1;
106
107     /**
108      * The service token
109      */
110     private ServiceToken mToken;
111
112     /**
113      * The query
114      */
115     private String mFilterString;
116
117     /**
118      * List view
119      */
120     private ListView mListView;
121
122     /**
123      * Used the filter the user's music
124      */
125     private SearchView mSearchView;
126
127     /**
128      * IME manager
129      */
130     private InputMethodManager mImm;
131
132     /**
133      * The view that container the no search results text and the loading progress bar
134      */
135     private LoadingEmptyContainer mLoadingEmptyContainer;
136
137     /**
138      * List view adapter
139      */
140     private SectionAdapter<SearchResult, SummarySearchAdapter> mAdapter;
141
142     /**
143      * boolean tracking whether this is the search level when the user first enters search
144      * or if the user has clicked show all
145      */
146     private boolean mTopLevelSearch;
147
148     /**
149      * If the user has clicked show all, this tells us what type (Artist, Album, etc)
150      */
151     private ResultType mSearchType;
152
153     /**
154      * Search History loader callback
155      */
156     private SearchHistoryCallback mSearchHistoryCallback;
157
158     /**
159      * List view
160      */
161     private ListView mSearchHistoryListView;
162
163     /**
164      * This tracks our current visible state between the different views
165       */
166     enum VisibleState {
167         SearchHistory,
168         Empty,
169         SearchResults,
170         Loading,
171     }
172
173     private VisibleState mCurrentState;
174
175     /**
176      * Handler for posting runnables
177      */
178     private Handler mHandler;
179
180     /**
181      * A runnable to show the loading view that will be posted with a delay to prevent flashing
182      */
183     private Runnable mLoadingRunnable;
184
185     /**
186      * Flag used to track if we are quitting so we don't flash loaders while finishing the activity
187      */
188     private boolean mQuitting = false;
189
190     /**
191      * Pop up menu helper
192      */
193     private PopupMenuHelper mPopupMenuHelper;
194
195     /**
196      * {@inheritDoc}
197      */
198     @Override
199     public void onCreate(final Bundle savedInstanceState) {
200         super.onCreate(savedInstanceState);
201
202         mPopupMenuHelper = new PopupMenuHelper(this, getSupportFragmentManager()) {
203             private SearchResult mSelectedItem;
204
205             @Override
206             public PopupMenuType onPreparePopupMenu(int position) {
207                 mSelectedItem = mAdapter.getTItem(position);
208
209                 return PopupMenuType.SearchResult;
210             }
211
212             @Override
213             protected long[] getIdList() {
214                 switch (mSelectedItem.mType) {
215                     case Artist:
216                         return MusicUtils.getSongListForArtist(SearchActivity.this,
217                                 mSelectedItem.mId);
218                     case Album:
219                         return MusicUtils.getSongListForAlbum(SearchActivity.this,
220                                 mSelectedItem.mId);
221                     case Song:
222                         return new long[] { mSelectedItem.mId };
223                     case Playlist:
224                         return MusicUtils.getSongListForPlaylist(SearchActivity.this,
225                                 mSelectedItem.mId);
226                     default:
227                         return null;
228                 }
229             }
230
231             @Override
232             protected long getSourceId() {
233                 return mSelectedItem.mId;
234             }
235
236             @Override
237             protected Config.IdType getSourceType() {
238                 return mSelectedItem.mType.getSourceType();
239             }
240
241             @Override
242             protected void updateMenuIds(PopupMenuType type, TreeSet<Integer> set) {
243                 super.updateMenuIds(type, set);
244
245                 if (mSelectedItem.mType == ResultType.Album) {
246                     set.add(FragmentMenuItems.MORE_BY_ARTIST);
247                 }
248             }
249
250             @Override
251             protected String getArtistName() {
252                 return mSelectedItem.mArtist;
253             }
254         };
255
256         // Fade it in
257         overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
258
259         // Control the media volume
260         setVolumeControlStream(AudioManager.STREAM_MUSIC);
261
262         // Bind Apollo's service
263         mToken = MusicUtils.bindToService(this, this);
264
265         // Set the layout
266         setContentView(R.layout.activity_search);
267
268         // get the input method manager
269         mImm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
270
271         // Initialize the adapter
272         SummarySearchAdapter adapter = new SummarySearchAdapter(this);
273         mAdapter = new SectionAdapter<SearchResult, SummarySearchAdapter>(this, adapter);
274         // Set the prefix
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() {
279             @Override
280             public void onPopupMenuClicked(View v, int position) {
281                 mPopupMenuHelper.showPopupMenu(v, position);
282             }
283         });
284
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);
290
291         initListView();
292
293         // setup handler and runnable
294         mHandler = new Handler();
295         mLoadingRunnable = new Runnable() {
296             @Override
297             public void run() {
298                 setState(VisibleState.Loading);
299             }
300         };
301
302         // Theme the action bar
303         final ActionBar actionBar = getActionBar();
304         actionBar.setDisplayHomeAsUpEnabled(true);
305
306         // Get the query String
307         mFilterString = getIntent().getStringExtra(SearchManager.QUERY);
308
309         // if we have a non-empty search string, this is a 2nd lvl search
310         if (!TextUtils.isEmpty(mFilterString)) {
311             mTopLevelSearch = false;
312
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];
317             }
318
319             int resourceId = 0;
320             switch (mSearchType) {
321                 case Artist:
322                     resourceId = R.string.search_title_artists;
323                     break;
324                 case Album:
325                     resourceId = R.string.search_title_albums;
326                     break;
327                 case Playlist:
328                     resourceId = R.string.search_title_playlists;
329                     break;
330                 case Song:
331                     resourceId = R.string.search_title_songs;
332                     break;
333             }
334             actionBar.setTitle(getString(resourceId, mFilterString).toUpperCase());
335             actionBar.setDisplayHomeAsUpEnabled(true);
336
337             // Set the prefix
338             mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
339
340             // Start the loader for the query
341             getSupportLoaderManager().initLoader(SEARCH_LOADER, null, this);
342         } else {
343             mTopLevelSearch = true;
344             mSearchHistoryCallback = new SearchHistoryCallback();
345
346             // Start the loader for the search history
347             getSupportLoaderManager().initLoader(HISTORY_LOADER, null, mSearchHistoryCallback);
348         }
349
350         // set the background on the root view
351         getWindow().getDecorView().getRootView().setBackgroundColor(
352                 getResources().getColor(R.color.background_color));
353     }
354
355     /**
356      * Sets up the list view
357      */
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
373         // to this later
374         //mListView.setEmptyView(mLoadingEmptyContainer);
375
376         // load the search history list view
377         mSearchHistoryListView = (ListView)findViewById(R.id.list_search_history);
378         mSearchHistoryListView.setOnItemClickListener(new OnItemClickListener() {
379             @Override
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);
383             }
384         });
385         mSearchHistoryListView.setOnTouchListener(this);
386     }
387
388     /**
389      * {@inheritDoc}
390      */
391     @Override
392     public Loader<SectionListContainer<SearchResult>> onCreateLoader(final int id,
393                                                                      final Bundle args) {
394         IItemCompare<SearchResult> comparator = null;
395
396         // prep the loader in case the query takes a long time
397         setLoading();
398
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);
403         }
404
405         return new SectionCreator<SearchResult>(this,
406                 new SummarySearchLoader(this, mFilterString, mSearchType),
407                 comparator);
408     }
409
410     /**
411      * {@inheritDoc}
412      */
413     @Override
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);
418         }
419
420         // Search view
421         getMenuInflater().inflate(R.menu.search, menu);
422
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());
428
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));
437
438         searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
439             @Override
440             public boolean onMenuItemActionExpand(MenuItem item) {
441                 return true;
442             }
443
444             @Override
445             public boolean onMenuItemActionCollapse(MenuItem item) {
446                 quit();
447                 return false;
448             }
449         });
450
451         menu.findItem(R.id.menu_search).expandActionView();
452
453         return super.onCreateOptionsMenu(menu);
454     }
455
456     private void quit() {
457         mQuitting = true;
458         finish();
459     }
460
461     /**
462      * {@inheritDoc}
463      */
464     @Override
465     protected void onDestroy() {
466         super.onDestroy();
467         // Unbind from the service
468         if (mService != null) {
469             MusicUtils.unbindFromService(mToken);
470             mToken = null;
471         }
472     }
473
474     /**
475      * {@inheritDoc}
476      */
477     @Override
478     public boolean onOptionsItemSelected(final MenuItem item) {
479         switch (item.getItemId()) {
480             case android.R.id.home:
481                 quit();
482                 return true;
483             default:
484                 break;
485         }
486         return super.onOptionsItemSelected(item);
487     }
488
489     /**
490      * {@inheritDoc}
491      */
492     @Override
493     public void onLoadFinished(final Loader<SectionListContainer<SearchResult>> loader,
494                                final SectionListContainer<SearchResult> data) {
495         // Check for any errors
496         if (data.mListResults.isEmpty()) {
497             // clear the adapter
498             mAdapter.clear();
499             // show the empty state
500             setState(VisibleState.Empty);
501         } else {
502             // Set the data
503             mAdapter.setData(data);
504             // show the search results
505             setState(VisibleState.SearchResults);
506         }
507     }
508
509     /**
510      * {@inheritDoc}
511      */
512     @Override
513     public void onLoaderReset(final Loader<SectionListContainer<SearchResult>> loader) {
514         mAdapter.unload();
515     }
516
517     /**
518      * {@inheritDoc}
519      */
520     @Override
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);
525         } else {
526             mAdapter.getUnderlyingAdapter().setPauseDiskCache(false);
527             mAdapter.notifyDataSetChanged();
528         }
529     }
530
531     /**
532      * {@inheritDoc}
533      */
534     @Override
535     public boolean onQueryTextSubmit(final String query) {
536         // simulate an on query text change
537         onQueryTextChange(query);
538         // hide the input manager
539         hideInputManager();
540
541         return true;
542     }
543
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) {
548             if (mImm != null) {
549                 mImm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
550             }
551             mSearchView.clearFocus();
552
553             // add our search string
554             SearchHistory.getInstance(this).addSearchString(mFilterString);
555         }
556     }
557
558     /**
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
561      */
562     public void setLoading() {
563         if (mCurrentState != VisibleState.Loading) {
564             if (!mHandler.hasCallbacks(mLoadingRunnable)) {
565                 mHandler.postDelayed(mLoadingRunnable, LOADING_DELAY);
566             }
567         }
568     }
569
570     /**
571      * Sets the currently visible view
572      * @param state the current visible state
573      */
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);
578
579         // if we are already looking at view already, just quit
580         if (mCurrentState == state) {
581             return;
582         }
583
584         mCurrentState = state;
585
586         mSearchHistoryListView.setVisibility(View.INVISIBLE);
587         mListView.setVisibility(View.INVISIBLE);
588         mLoadingEmptyContainer.setVisibility(View.INVISIBLE);
589
590         switch (mCurrentState) {
591             case SearchHistory:
592                 mSearchHistoryListView.setVisibility(View.VISIBLE);
593                 break;
594             case SearchResults:
595                 mListView.setVisibility(View.VISIBLE);
596                 break;
597             case Empty:
598                 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
599                 mLoadingEmptyContainer.showNoResults();
600                 break;
601             case Loading:
602                 mLoadingEmptyContainer.setVisibility(View.VISIBLE);
603                 mLoadingEmptyContainer.showLoading();
604                 break;
605         }
606     }
607
608     /**
609      * {@inheritDoc}
610      */
611     @Override
612     public boolean onQueryTextChange(final String newText) {
613         if (mQuitting) {
614             return true;
615         }
616
617         if (TextUtils.isEmpty(newText)) {
618             if (!TextUtils.isEmpty(mFilterString)) {
619                 mFilterString = "";
620                 getSupportLoaderManager().restartLoader(HISTORY_LOADER, null,
621                         mSearchHistoryCallback);
622                 getSupportLoaderManager().destroyLoader(SEARCH_LOADER);
623             }
624
625             return true;
626         }
627
628         // if the strings are the same, return
629         if (newText.equals(mFilterString)) {
630             return true;
631         }
632
633         // Called when the action bar search text has changed. Update
634         // the search filter, and restart the loader to do a new query
635         // with this filter.
636         mFilterString = newText;
637         // Set the prefix
638         mAdapter.getUnderlyingAdapter().setPrefix(mFilterString);
639         getSupportLoaderManager().restartLoader(SEARCH_LOADER, null, this);
640         getSupportLoaderManager().destroyLoader(HISTORY_LOADER);
641         return true;
642     }
643
644     /**
645      * {@inheritDoc}
646      */
647     @Override
648     public void onItemClick(final AdapterView<?> parent, final View view, final int position,
649             final long id) {
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);
658         } else {
659             SearchResult item = mAdapter.getTItem(position);
660             switch (item.mType) {
661                 case Artist:
662                     NavUtils.openArtistProfile(this, item.mArtist);
663                     break;
664                 case Album:
665                     NavUtils.openAlbumProfile(this, item.mAlbum, item.mArtist, item.mId);
666                     break;
667                 case Playlist:
668                     NavUtils.openPlaylist(this, item.mId, item.mTitle);
669                     break;
670                 case Song:
671                     // If it's a song, play it and leave
672                     final long[] list = new long[]{
673                             item.mId
674                     };
675                     MusicUtils.playAll(this, list, 0, -1, Config.IdType.NA, false);
676                     break;
677             }
678         }
679     }
680
681     /**
682      * {@inheritDoc}
683      */
684     @Override
685     public void onServiceConnected(final ComponentName name, final IBinder service) {
686         mService = IElevenService.Stub.asInterface(service);
687     }
688
689     /**
690      * {@inheritDoc}
691      */
692     @Override
693     public void onServiceDisconnected(final ComponentName name) {
694         mService = null;
695     }
696
697     /**
698      * This class loads a search result summary of items
699      */
700     private static final class SummarySearchLoader extends SimpleListLoader<SearchResult> {
701         private final String mQuery;
702         private final ResultType mSearchType;
703
704         public SummarySearchLoader(final Context context, final String query,
705                                    final ResultType searchType) {
706             super(context);
707             mQuery = query;
708             mSearchType = searchType;
709         }
710
711         /**
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
716          */
717         protected SearchResult createSearchResult(final Cursor cursor, ResultType type) {
718             SearchResult item = null;
719
720             switch (type) {
721                 case Playlist:
722                     item = SearchResult.createPlaylistResult(cursor);
723                     item.mSongCount = MusicUtils.getSongCountForPlaylist(getContext(), item.mId);
724                     break;
725                 case Song:
726                     item = SearchResult.createSearchResult(cursor);
727                     if (item != null) {
728                         AlbumArtistDetails details = MusicUtils.getAlbumArtDetails(getContext(),
729                                 item.mId);
730                         if (details != null) {
731                             item.mArtist = details.mArtistName;
732                             item.mAlbum = details.mAlbumName;
733                             item.mAlbumId = details.mAlbumId;
734                         }
735                     }
736                     break;
737                 case Album:
738                 case Artist:
739                 default:
740                     item = SearchResult.createSearchResult(cursor);
741                     break;
742             }
743
744             return item;
745         }
746
747         @Override
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();
752             }
753
754             return runGenericSearch();
755         }
756
757         /**
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
761          */
762         protected List<SearchResult> runSearchForType() {
763             ArrayList<SearchResult> results = new ArrayList<SearchResult>();
764             Cursor cursor = null;
765             try {
766                 if (mSearchType == ResultType.Playlist) {
767                     cursor = makePlaylistSearchCursor(getContext(), mQuery);
768                 } else {
769                     cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
770                 }
771
772                 // pre-cache this index
773                 final int mimeTypeIndex = cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE);
774
775                 if (cursor != null && cursor.moveToFirst()) {
776                     do {
777                         boolean addResult = true;
778
779                         if (mSearchType != ResultType.Playlist) {
780                             // get the result type
781                             ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
782                             if (type != mSearchType) {
783                                 addResult = false;
784                             }
785                         }
786
787                         if (addResult) {
788                             results.add(createSearchResult(cursor, mSearchType));
789                         }
790                     } while (cursor.moveToNext());
791                 }
792
793             } finally {
794                 if (cursor != null) {
795                     cursor.close();
796                     cursor = null;
797                 }
798             }
799
800             return results;
801         }
802
803         /**
804          * This will run a search given a filter string and return the top NUM_RESULTS_TO_GET per
805          * type
806          * @return the results for that search
807          */
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();
812
813             // number of results we want
814             final int numResultsNeeded = Config.SEARCH_NUM_RESULTS_TO_GET * numTypes;
815
816             // current number of results we have
817             int numResultsAdded = 0;
818
819             // count for each result type
820             int[] numOfEachType = new int[numTypes];
821
822             // search playlists first
823             Cursor playlistCursor = makePlaylistSearchCursor(getContext(), mQuery);
824             if (playlistCursor != null && playlistCursor.moveToFirst()) {
825                 do {
826                     // create the item
827                     SearchResult item = createSearchResult(playlistCursor, ResultType.Playlist);
828                     /// add the results
829                     numResultsAdded++;
830                     results.add(item);
831                 } while (playlistCursor.moveToNext()
832                         && numResultsAdded < Config.SEARCH_NUM_RESULTS_TO_GET);
833
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;
838
839                 // close the cursor
840                 playlistCursor.close();
841                 playlistCursor = null;
842             }
843
844             // do fancy audio search
845             Cursor cursor = ApolloUtils.createSearchQueryCursor(getContext(), mQuery);
846
847             // pre-cache this index
848             final int mimeTypeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE);
849
850             // walk through the cursor
851             if (cursor != null && cursor.moveToFirst()) {
852                 do {
853                     // get the result type
854                     ResultType type = ResultType.getResultType(cursor, mimeTypeIndex);
855
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);
860
861                         if (item != null) {
862                             // add it
863                             results.add(item);
864                             numOfEachType[type.ordinal()]++;
865                             numResultsAdded++;
866
867                             // if we have enough then quit
868                             if (numResultsAdded >= numResultsNeeded) {
869                                 break;
870                             }
871                         }
872                     }
873                 } while (cursor.moveToNext());
874
875                 cursor.close();
876                 cursor = null;
877             }
878
879             // sort our results
880             Collections.sort(results, SearchResult.COMPARATOR);
881
882             return results;
883         }
884
885         public static Cursor makePlaylistSearchCursor(final Context context,
886                                                       final String searchTerms) {
887             if (TextUtils.isEmpty(searchTerms)) {
888                 return null;
889             }
890
891             // trim out special characters like % or \ as well as things like "a" "and" etc
892             String trimmedSearchTerms = MusicUtils.getTrimmedName(searchTerms);
893
894             if (TextUtils.isEmpty(trimmedSearchTerms)) {
895                 return null;
896             }
897
898             String[] keywords = trimmedSearchTerms.split(" ");
899
900             // prep the keyword for like search
901             for (int i = 0; i < keywords.length; i++) {
902                 keywords[i] = "%" + keywords[i] + "%";
903             }
904
905             String where = "";
906
907             // make the where clause
908             for (int i = 0; i < keywords.length; i++) {
909                 if (i == 0) {
910                     where = "name LIKE ?";
911                 } else {
912                     where += " AND name LIKE ?";
913                 }
914             }
915
916             return context.getContentResolver().query(
917                     MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
918                     new String[]{
919                         /* 0 */
920                             BaseColumns._ID,
921                         /* 1 */
922                             MediaStore.Audio.PlaylistsColumns.NAME
923                     }, where, keywords, MediaStore.Audio.Playlists.DEFAULT_SORT_ORDER);
924         }
925     }
926
927     /**
928      * Loads the search history in the background and creates an array adapter
929      */
930     public static class SearchHistoryLoader extends WrappedAsyncTaskLoader<ArrayAdapter<String>> {
931         public SearchHistoryLoader(Context context) {
932             super(context);
933         }
934
935         @Override
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);
941             return adapter;
942         }
943     }
944
945     /**
946      * This handles the Loader callbacks for the search history
947      */
948     public class SearchHistoryCallback implements LoaderCallbacks<ArrayAdapter<String>> {
949         @Override
950         public Loader<ArrayAdapter<String>> onCreateLoader(int i, Bundle bundle) {
951             // prep the loader in case the query takes a long time
952             setLoading();
953
954             return new SearchHistoryLoader(SearchActivity.this);
955         }
956
957         @Override
958         public void onLoadFinished(Loader<ArrayAdapter<String>> searchHistoryAdapterLoader,
959                                    ArrayAdapter<String> searchHistoryAdapter) {
960             // show the search history
961             setState(VisibleState.SearchHistory);
962
963             mSearchHistoryListView.setAdapter(searchHistoryAdapter);
964         }
965
966         @Override
967         public void onLoaderReset(Loader<ArrayAdapter<String>> cursorAdapterLoader) {
968             ((ArrayAdapter)mSearchHistoryListView.getAdapter()).clear();
969         }
970     }
971
972     /**
973      * {@inheritDoc}
974      */
975     @Override
976     public void onScroll(final AbsListView view, final int firstVisibleItem,
977             final int visibleItemCount, final int totalItemCount) {
978         // Nothing to do
979     }
980
981     @Override
982     public boolean onTouch(View v, MotionEvent event) {
983         hideInputManager();
984         return false;
985     }
986 }