OSDN Git Service

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