OSDN Git Service

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