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.
12 package com.cyngn.eleven.ui.activities;
14 import static com.cyngn.eleven.utils.MusicUtils.mService;
16 import android.app.Activity;
17 import android.app.ActionBar;
18 import android.app.LoaderManager.LoaderCallbacks;
19 import android.app.SearchManager;
20 import android.app.SearchableInfo;
21 import android.content.ComponentName;
22 import android.content.CursorLoader;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.Loader;
26 import android.content.ServiceConnection;
27 import android.database.Cursor;
28 import android.media.AudioManager;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.IBinder;
32 import android.provider.BaseColumns;
33 import android.provider.MediaStore;
34 import android.text.TextUtils;
35 import android.view.Menu;
36 import android.view.MenuItem;
37 import android.view.View;
38 import android.view.ViewGroup;
39 import android.view.Window;
40 import android.view.inputmethod.InputMethodManager;
41 import android.widget.AbsListView;
42 import android.widget.AbsListView.OnScrollListener;
43 import android.widget.AdapterView;
44 import android.widget.AdapterView.OnItemClickListener;
45 import android.widget.CursorAdapter;
46 import android.widget.FrameLayout;
47 import android.widget.GridView;
48 import android.widget.ImageView.ScaleType;
49 import android.widget.SearchView;
50 import android.widget.SearchView.OnQueryTextListener;
51 import android.widget.TextView;
53 import com.cyngn.eleven.IElevenService;
54 import com.cyngn.eleven.R;
55 import com.cyngn.eleven.cache.ImageFetcher;
56 import com.cyngn.eleven.format.PrefixHighlighter;
57 import com.cyngn.eleven.recycler.RecycleHolder;
58 import com.cyngn.eleven.ui.MusicHolder;
59 import com.cyngn.eleven.utils.ApolloUtils;
60 import com.cyngn.eleven.utils.MusicUtils;
61 import com.cyngn.eleven.utils.MusicUtils.ServiceToken;
62 import com.cyngn.eleven.utils.NavUtils;
63 import com.cyngn.eleven.utils.ThemeUtils;
65 import java.util.Locale;
68 * Provides the search interface for Apollo.
70 * @author Andrew Neal (andrewdneal@gmail.com)
72 public class SearchActivity extends Activity implements LoaderCallbacks<Cursor>,
73 OnScrollListener, OnQueryTextListener, OnItemClickListener, ServiceConnection {
75 * Grid view column count. ONE - list, TWO - normal grid
77 private static final int ONE = 1, TWO = 2;
82 private ServiceToken mToken;
87 private String mFilterString;
92 private GridView mGridView;
97 private SearchAdapter mAdapter;
99 // Used the filter the user's music
100 private SearchView mSearchView;
105 private ThemeUtils mResources;
110 @SuppressWarnings("deprecation")
112 protected void onCreate(final Bundle savedInstanceState) {
113 super.onCreate(savedInstanceState);
115 // Initialze the theme resources
116 mResources = new ThemeUtils(this);
117 // Set the overflow style
118 mResources.setOverflowStyle(this);
121 overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
123 // Control the media volume
124 setVolumeControlStream(AudioManager.STREAM_MUSIC);
126 // Bind Apollo's service
127 mToken = MusicUtils.bindToService(this, this);
129 // Theme the action bar
130 final ActionBar actionBar = getActionBar();
131 mResources.themeActionBar(actionBar, getString(R.string.app_name));
132 actionBar.setDisplayHomeAsUpEnabled(true);
135 setContentView(R.layout.grid_base);
137 // Give the background a little UI
138 final FrameLayout background = (FrameLayout)findViewById(R.id.grid_base_container);
139 background.setBackgroundDrawable(getResources().getDrawable(R.drawable.pager_background));
142 final String query = getIntent().getStringExtra(SearchManager.QUERY);
143 mFilterString = !TextUtils.isEmpty(query) ? query : null;
145 // Action bar subtitle
146 mResources.setSubtitle("\"" + mFilterString + "\"");
148 // Initialize the adapter
149 mAdapter = new SearchAdapter(this);
151 mAdapter.setPrefix(mFilterString);
152 // Initialze the list
153 mGridView = (GridView)findViewById(R.id.grid_base);
155 mGridView.setAdapter(mAdapter);
157 mGridView.setRecyclerListener(new RecycleHolder());
158 // Seepd up scrolling
159 mGridView.setOnScrollListener(this);
160 mGridView.setOnItemClickListener(this);
161 if (ApolloUtils.isLandscape(this)) {
162 mGridView.setNumColumns(TWO);
164 mGridView.setNumColumns(ONE);
166 // Prepare the loader. Either re-connect with an existing one,
167 // or start a new one.
168 getLoaderManager().initLoader(0, null, this);
175 protected void onNewIntent(final Intent intent) {
176 super.onNewIntent(intent);
177 final String query = intent.getStringExtra(SearchManager.QUERY);
178 mFilterString = !TextUtils.isEmpty(query) ? query : null;
180 mAdapter.setPrefix(mFilterString);
181 getLoaderManager().restartLoader(0, null, this);
188 public boolean onCreateOptionsMenu(final Menu menu) {
190 getMenuInflater().inflate(R.menu.search, menu);
191 // Theme the search icon
192 mResources.setSearchIcon(menu);
194 // Filter the list the user is looking it via SearchView
195 mSearchView = (SearchView)menu.findItem(R.id.menu_search).getActionView();
196 mSearchView.setOnQueryTextListener(this);
199 final SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
200 final SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
201 mSearchView.setSearchableInfo(searchableInfo);
202 return super.onCreateOptionsMenu(menu);
209 protected void onStart() {
211 MusicUtils.notifyForegroundStateChanged(this, true);
218 protected void onStop() {
220 MusicUtils.notifyForegroundStateChanged(this, false);
227 protected void onDestroy() {
229 // Unbind from the service
230 if (mService != null) {
231 MusicUtils.unbindFromService(mToken);
240 public boolean onOptionsItemSelected(final MenuItem item) {
241 switch (item.getItemId()) {
242 case android.R.id.home:
248 return super.onOptionsItemSelected(item);
255 public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
256 final Uri uri = Uri.parse("content://media/external/audio/search/fancy/"
257 + Uri.encode(mFilterString));
258 final String[] projection = new String[] {
259 BaseColumns._ID, MediaStore.Audio.Media.MIME_TYPE, MediaStore.Audio.Artists.ARTIST,
260 MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Media.TITLE, "data1", "data2"
262 return new CursorLoader(this, uri, projection, null, null, null);
269 public void onLoadFinished(final Loader<Cursor> loader, final Cursor data) {
270 if (data == null || data.isClosed() || data.getCount() <= 0) {
271 // Set the empty text
272 final TextView empty = (TextView)findViewById(R.id.empty);
273 empty.setText(getString(R.string.empty_search));
274 mGridView.setEmptyView(empty);
277 // Swap the new cursor in. (The framework will take care of closing the
278 // old cursor once we return.)
279 mAdapter.swapCursor(data);
286 public void onLoaderReset(final Loader<Cursor> loader) {
287 // This is called when the last Cursor provided to onLoadFinished()
288 // above is about to be closed. We need to make sure we are no
290 mAdapter.swapCursor(null);
297 public void onScrollStateChanged(final AbsListView view, final int scrollState) {
298 // Pause disk cache access to ensure smoother scrolling
299 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING
300 || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
301 mAdapter.setPauseDiskCache(true);
303 mAdapter.setPauseDiskCache(false);
304 mAdapter.notifyDataSetChanged();
312 public boolean onQueryTextSubmit(final String query) {
313 if (TextUtils.isEmpty(query)) {
316 // When the search is "committed" by the user, then hide the keyboard so
318 // more easily browse the list of results.
319 if (mSearchView != null) {
320 final InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
322 imm.hideSoftInputFromWindow(mSearchView.getWindowToken(), 0);
324 mSearchView.clearFocus();
326 // Action bar subtitle
327 mResources.setSubtitle("\"" + mFilterString + "\"");
335 public boolean onQueryTextChange(final String newText) {
336 if (TextUtils.isEmpty(newText)) {
339 // Called when the action bar search text has changed. Update
340 // the search filter, and restart the loader to do a new query
342 mFilterString = !TextUtils.isEmpty(newText) ? newText : null;
344 mAdapter.setPrefix(mFilterString);
345 getLoaderManager().restartLoader(0, null, this);
353 public void onItemClick(final AdapterView<?> parent, final View view, final int position,
355 Cursor cursor = mAdapter.getCursor();
356 cursor.moveToPosition(position);
357 if (cursor.isBeforeFirst() || cursor.isAfterLast()) {
361 final String mimeType = cursor.getString(cursor
362 .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
364 // If it's an artist, open the artist profile
365 if ("artist".equals(mimeType)) {
366 NavUtils.openArtistProfile(this,
367 cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST)));
368 } else if ("album".equals(mimeType)) {
369 // If it's an album, open the album profile
370 NavUtils.openAlbumProfile(this,
371 cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)),
372 cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST)),
373 cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID)));
374 } else if (position >= 0 && id >= 0) {
375 // If it's a song, play it and leave
376 final long[] list = new long[] {
379 MusicUtils.playAll(this, list, 0, false);
393 public void onServiceConnected(final ComponentName name, final IBinder service) {
394 mService = IElevenService.Stub.asInterface(service);
401 public void onServiceDisconnected(final ComponentName name) {
406 * Used to populate the list view with the search results.
408 private static final class SearchAdapter extends CursorAdapter {
411 * Number of views (ImageView and TextView)
413 private static final int VIEW_TYPE_COUNT = 2;
416 * Image cache and image fetcher
418 private final ImageFetcher mImageFetcher;
421 * Highlights the query
423 private final PrefixHighlighter mHighlighter;
426 * The prefix that's highlighted
428 private char[] mPrefix;
431 * Constructor for <code>SearchAdapter</code>
433 * @param context The {@link Context} to use.
435 public SearchAdapter(final Activity context) {
436 super(context, null, false);
437 // Initialize the cache & image fetcher
438 mImageFetcher = ApolloUtils.getImageFetcher(context);
439 // Create the prefix highlighter
440 mHighlighter = new PrefixHighlighter(context);
447 public void bindView(final View convertView, final Context context, final Cursor cursor) {
448 /* Recycle ViewHolder's items */
449 MusicHolder holder = (MusicHolder)convertView.getTag();
450 if (holder == null) {
451 holder = new MusicHolder(convertView);
452 convertView.setTag(holder);
456 final String mimetype = cursor.getString(cursor
457 .getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE));
459 if (mimetype.equals("artist")) {
460 holder.mImage.get().setScaleType(ScaleType.CENTER_CROP);
462 // Get the artist name
463 final String artist = cursor.getString(cursor
464 .getColumnIndexOrThrow(MediaStore.Audio.Artists.ARTIST));
465 holder.mLineOne.get().setText(artist);
467 // Get the album count
468 final int albumCount = cursor.getInt(cursor.getColumnIndexOrThrow("data1"));
469 holder.mLineTwo.get().setText(
470 MusicUtils.makeLabel(context, R.plurals.Nalbums, albumCount));
472 // Get the song count
473 final int songCount = cursor.getInt(cursor.getColumnIndexOrThrow("data2"));
474 holder.mLineThree.get().setText(
475 MusicUtils.makeLabel(context, R.plurals.Nsongs, songCount));
477 // Asynchronously load the artist image into the adapter
478 mImageFetcher.loadArtistImage(artist, holder.mImage.get());
480 // Highlght the query
481 mHighlighter.setText(holder.mLineOne.get(), artist, mPrefix);
482 } else if (mimetype.equals("album")) {
483 holder.mImage.get().setScaleType(ScaleType.FIT_XY);
485 // Get the Id of the album
486 final long id = cursor.getLong(cursor
487 .getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
489 // Get the album name
490 final String album = cursor.getString(cursor
491 .getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
492 holder.mLineOne.get().setText(album);
494 // Get the artist name
495 final String artist = cursor.getString(cursor
496 .getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
497 holder.mLineTwo.get().setText(artist);
499 // Asynchronously load the album images into the adapter
500 mImageFetcher.loadAlbumImage(artist, album, id, holder.mImage.get());
501 // Asynchronously load the artist image into the adapter
502 mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
504 // Highlght the query
505 mHighlighter.setText(holder.mLineOne.get(), album, mPrefix);
507 } else if (mimetype.startsWith("audio/") || mimetype.equals("application/ogg")
508 || mimetype.equals("application/x-ogg")) {
509 holder.mImage.get().setScaleType(ScaleType.FIT_XY);
510 holder.mImage.get().setImageResource(R.drawable.header_temp);
512 // Get the track name
513 final String track = cursor.getString(cursor
514 .getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
515 holder.mLineOne.get().setText(track);
517 // Get the album name
518 final String album = cursor.getString(cursor
519 .getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
520 holder.mLineTwo.get().setText(album);
522 final String artist = cursor.getString(cursor
523 .getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
524 // Asynchronously load the artist image into the adapter
525 mImageFetcher.loadArtistImage(artist, holder.mBackground.get());
526 holder.mLineThree.get().setText(artist);
528 // Highlght the query
529 mHighlighter.setText(holder.mLineOne.get(), track, mPrefix);
537 public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
538 return ((Activity)context).getLayoutInflater().inflate(
539 R.layout.list_item_detailed, parent, false);
546 public boolean hasStableIds() {
554 public int getViewTypeCount() {
555 return VIEW_TYPE_COUNT;
559 * @param pause True to temporarily pause the disk cache, false
562 public void setPauseDiskCache(final boolean pause) {
563 if (mImageFetcher != null) {
564 mImageFetcher.setPauseDiskCache(pause);
569 * @param prefix The query to filter.
571 public void setPrefix(final CharSequence prefix) {
572 if (!TextUtils.isEmpty(prefix)) {
573 mPrefix = prefix.toString().toUpperCase(Locale.getDefault()).toCharArray();
584 public void onScroll(final AbsListView view, final int firstVisibleItem,
585 final int visibleItemCount, final int totalItemCount) {