OSDN Git Service

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