2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.music;
19 import android.app.ListActivity;
20 import android.app.SearchManager;
21 import android.content.AsyncQueryHandler;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.res.Resources;
28 import android.database.Cursor;
29 import android.graphics.Bitmap;
30 import android.graphics.BitmapFactory;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.media.AudioManager;
34 import android.media.MediaFile;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.Handler;
38 import android.os.Message;
39 import android.provider.MediaStore;
40 import android.view.ContextMenu;
41 import android.view.Menu;
42 import android.view.MenuItem;
43 import android.view.SubMenu;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.Window;
47 import android.view.ContextMenu.ContextMenuInfo;
48 import android.widget.Adapter;
49 import android.widget.AlphabetIndexer;
50 import android.widget.CursorAdapter;
51 import android.widget.ImageView;
52 import android.widget.ListAdapter;
53 import android.widget.ListView;
54 import android.widget.SectionIndexer;
55 import android.widget.SimpleCursorAdapter;
56 import android.widget.TextView;
57 import android.widget.AdapterView.AdapterContextMenuInfo;
59 import java.text.Collator;
61 public class AlbumBrowserActivity extends ListActivity
62 implements View.OnCreateContextMenuListener, MusicUtils.Defs
64 private String mCurrentAlbumId;
65 private String mCurrentAlbumName;
66 private String mCurrentArtistNameForAlbum;
67 private MyQueryHandler mQueryHandler;
68 private String mFilterString = "";
69 private final static int SEARCH = CHILD_MENU_BASE;
71 public AlbumBrowserActivity()
75 /** Called when the activity is first created. */
77 public void onCreate(Bundle icicle)
80 mCurrentAlbumId = icicle.getString("selectedalbum");
81 mArtistId = icicle.getString("artist");
83 mArtistId = getIntent().getStringExtra("artist");
85 super.onCreate(icicle);
86 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
87 setVolumeControlStream(AudioManager.STREAM_MUSIC);
88 MusicUtils.bindToService(this);
90 IntentFilter f = new IntentFilter();
91 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
92 f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
93 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
94 f.addDataScheme("file");
95 registerReceiver(mScanListener, f);
97 mQueryHandler = new MyQueryHandler(getContentResolver());
98 mAlbumCursor = (Cursor) getLastNonConfigurationInstance();
99 if (mAlbumCursor == null) {
100 //Log.i("@@@", "starting query");
101 setTitle(R.string.working_albums);
102 getAlbumCursor(mQueryHandler);
110 public Object onRetainNonConfigurationInstance() {
111 Cursor c = mAlbumCursor;
116 class MyQueryHandler extends AsyncQueryHandler {
117 MyQueryHandler(ContentResolver res) {
122 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
123 //Log.i("@@@", "query complete");
130 public void onSaveInstanceState(Bundle outcicle) {
131 // need to store the selected item so we don't lose it in case
132 // of an orientation switch. Otherwise we could lose it while
133 // in the middle of specifying a playlist to add the item to.
134 outcicle.putString("selectedalbum", mCurrentAlbumId);
135 outcicle.putString("artist", mArtistId);
136 super.onSaveInstanceState(outcicle);
140 public void onDestroy() {
141 MusicUtils.unbindFromService(this);
142 if (mAlbumCursor != null) {
143 mAlbumCursor.close();
146 unregisterReceiver(mScanListener);
151 public void onResume() {
153 IntentFilter f = new IntentFilter();
154 f.addAction(MediaPlaybackService.META_CHANGED);
155 f.addAction(MediaPlaybackService.QUEUE_CHANGED);
156 registerReceiver(mTrackListListener, f);
157 mTrackListListener.onReceive(null, null);
159 MusicUtils.setSpinnerState(this);
162 private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
164 public void onReceive(Context context, Intent intent) {
165 getListView().invalidateViews();
168 private BroadcastReceiver mScanListener = new BroadcastReceiver() {
170 public void onReceive(Context context, Intent intent) {
171 MusicUtils.setSpinnerState(AlbumBrowserActivity.this);
172 mReScanHandler.sendEmptyMessage(0);
173 if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
174 MusicUtils.clearAlbumArtCache();
179 private Handler mReScanHandler = new Handler() {
181 public void handleMessage(Message msg) {
183 getAlbumCursor(mQueryHandler);
184 if (mAlbumCursor == null) {
185 sendEmptyMessageDelayed(0, 1000);
191 public void onPause() {
192 unregisterReceiver(mTrackListListener);
193 mReScanHandler.removeCallbacksAndMessages(null);
197 public void init(Cursor c) {
201 /* The UI spec does not call for icons in the title bar
202 if (mArtistId != null) {
203 requestWindowFeature(Window.FEATURE_LEFT_ICON);
206 setContentView(R.layout.media_picker_activity);
207 /* The UI spec does not call for icons in the title bar
208 if (mArtistId != null) {
209 // this call needs to happen after setContentView
210 setFeatureDrawableResource(Window.FEATURE_LEFT_ICON, R.drawable.person_title_icon);
214 if (mAlbumCursor == null) {
215 MusicUtils.displayDatabaseError(this);
220 // Map Cursor columns to views defined in media_list_item.xml
221 CursorAdapter adapter = (CursorAdapter) getListAdapter();
222 if (adapter == null) {
223 adapter = new AlbumListAdapter(
225 R.layout.track_list_item,
229 setListAdapter(adapter);
231 adapter.changeCursor(mAlbumCursor);
233 ListView lv = getListView();
234 lv.setOnCreateContextMenuListener(this);
235 lv.setTextFilterEnabled(true);
238 private void setTitle() {
239 CharSequence fancyName = "";
240 if (mAlbumCursor != null && mAlbumCursor.getCount() > 0) {
241 mAlbumCursor.moveToFirst();
242 fancyName = mAlbumCursor.getString(3);
243 if (MediaFile.UNKNOWN_STRING.equals(fancyName))
244 fancyName = getText(R.string.unknown_artist_name);
246 if (mArtistId != null && fancyName != null)
249 setTitle(R.string.albums_title);
251 setTitle(R.string.no_albums_title);
256 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
257 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
258 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
259 MusicUtils.makePlaylistMenu(this, sub);
260 menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
261 menu.add(0, SEARCH, 0, R.string.search_title);
263 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
264 mAlbumCursor.moveToPosition(mi.position);
265 mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
266 mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
267 mCurrentArtistNameForAlbum = mAlbumCursor.getString(
268 mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
269 menu.setHeaderTitle(mCurrentAlbumName);
273 public boolean onContextItemSelected(MenuItem item) {
274 switch (item.getItemId()) {
275 case PLAY_SELECTION: {
276 // play the selected album
277 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
278 MusicUtils.playAll(this, list, 0);
283 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
284 MusicUtils.addToCurrentPlaylist(this, list);
289 Intent intent = new Intent();
290 intent.setClass(this, CreatePlaylist.class);
291 startActivityForResult(intent, NEW_PLAYLIST);
295 case PLAYLIST_SELECTED: {
296 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
297 int playlist = item.getIntent().getIntExtra("playlist", 0);
298 MusicUtils.addToPlaylist(this, list, playlist);
302 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
303 String f = getString(R.string.delete_album_desc);
304 String desc = String.format(f, mCurrentAlbumName);
305 Bundle b = new Bundle();
306 b.putString("description", desc);
307 b.putIntArray("items", list);
308 Intent intent = new Intent();
309 intent.setClass(this, DeleteItems.class);
311 startActivityForResult(intent, -1);
319 return super.onContextItemSelected(item);
323 CharSequence title = null;
326 Intent i = new Intent();
327 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
329 title = mCurrentAlbumName;
330 query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
331 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
332 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
333 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
334 title = getString(R.string.mediasearch, title);
335 i.putExtra(SearchManager.QUERY, query);
337 startActivity(Intent.createChooser(i, title));
341 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
342 switch (requestCode) {
344 if (resultCode == RESULT_CANCELED) {
347 getAlbumCursor(mQueryHandler);
352 if (resultCode == RESULT_OK) {
353 Uri uri = intent.getData();
355 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
356 MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment()));
364 protected void onListItemClick(ListView l, View v, int position, long id)
369 Intent intent = new Intent(Intent.ACTION_PICK);
370 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
371 intent.putExtra("album", Long.valueOf(id).toString());
372 intent.putExtra("artist", mArtistId);
373 startActivity(intent);
377 public boolean onCreateOptionsMenu(Menu menu) {
378 super.onCreateOptionsMenu(menu);
379 menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
380 menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback);
381 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
386 public boolean onPrepareOptionsMenu(Menu menu) {
387 menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded());
388 return super.onPrepareOptionsMenu(menu);
392 public boolean onOptionsItemSelected(MenuItem item) {
395 switch (item.getItemId()) {
397 intent = new Intent();
398 intent.setClass(this, MusicBrowserActivity.class);
399 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
400 startActivity(intent);
404 intent = new Intent("com.android.music.PLAYBACK_VIEWER");
405 startActivity(intent);
409 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
410 new String [] { MediaStore.Audio.Media._ID},
411 MediaStore.Audio.Media.IS_MUSIC + "=1", null,
412 MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
413 if (cursor != null) {
414 MusicUtils.shuffleAll(this, cursor);
419 return super.onOptionsItemSelected(item);
422 private Cursor getAlbumCursor(AsyncQueryHandler async) {
423 StringBuilder where = new StringBuilder();
424 where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
426 // Add in the filtering constraints
427 String [] keywords = null;
428 if (mFilterString != null) {
429 String [] searchWords = mFilterString.split(" ");
430 keywords = new String[searchWords.length];
431 Collator col = Collator.getInstance();
432 col.setStrength(Collator.PRIMARY);
433 for (int i = 0; i < searchWords.length; i++) {
434 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
436 for (int i = 0; i < searchWords.length; i++) {
437 where.append(" AND ");
438 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
439 where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?");
443 String whereclause = where.toString();
445 String[] cols = new String[] {
446 MediaStore.Audio.Albums._ID,
447 MediaStore.Audio.Albums.ALBUM,
448 MediaStore.Audio.Albums.ALBUM_KEY,
449 MediaStore.Audio.Albums.ARTIST,
450 MediaStore.Audio.Albums.NUMBER_OF_SONGS,
451 MediaStore.Audio.Albums.ALBUM_ART
454 if (mArtistId != null) {
456 async.startQuery(0, null,
457 MediaStore.Audio.Artists.Albums.getContentUri("external",
458 Long.valueOf(mArtistId)),
459 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
461 ret = MusicUtils.query(this,
462 MediaStore.Audio.Artists.Albums.getContentUri("external",
463 Long.valueOf(mArtistId)),
464 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
468 async.startQuery(0, null,
469 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
470 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
472 ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
473 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
479 class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer {
481 private final Drawable mNowPlayingOverlay;
482 private final BitmapDrawable mDefaultAlbumIcon;
483 private final int mAlbumIdx;
484 private final int mArtistIdx;
485 private final int mNumSongsIdx;
486 private final int mAlbumArtIndex;
487 private final Resources mResources;
488 private final StringBuilder mStringBuilder = new StringBuilder();
489 private final String mUnknownAlbum;
490 private final String mUnknownArtist;
491 private final String mAlbumSongSeparator;
492 private final Object[] mFormatArgs = new Object[1];
493 private final AlphabetIndexer mIndexer;
499 ImageView play_indicator;
503 AlbumListAdapter(Context context, int layout, Cursor cursor, String[] from, int[] to) {
504 super(context, layout, cursor, from, to);
506 mUnknownAlbum = context.getString(R.string.unknown_album_name);
507 mUnknownArtist = context.getString(R.string.unknown_artist_name);
508 mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
510 Resources r = getResources();
511 mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
513 Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list);
514 mDefaultAlbumIcon = new BitmapDrawable(b);
515 // no filter or dither, it's a lot faster and we can't tell the difference
516 mDefaultAlbumIcon.setFilterBitmap(false);
517 mDefaultAlbumIcon.setDither(false);
518 mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
519 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST);
520 mNumSongsIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS);
521 mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART);
523 mResources = context.getResources();
525 mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString(
526 com.android.internal.R.string.fast_scroll_alphabet));
530 public View newView(Context context, Cursor cursor, ViewGroup parent) {
531 View v = super.newView(context, cursor, parent);
532 ViewHolder vh = new ViewHolder();
533 vh.line1 = (TextView) v.findViewById(R.id.line1);
534 vh.line2 = (TextView) v.findViewById(R.id.line2);
535 vh.duration = (TextView) v.findViewById(R.id.duration);
536 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
537 vh.icon = (ImageView) v.findViewById(R.id.icon);
538 vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
539 vh.icon.setPadding(1, 1, 1, 1);
545 public void bindView(View view, Context context, Cursor cursor) {
547 ViewHolder vh = (ViewHolder) view.getTag();
549 String name = cursor.getString(mAlbumIdx);
550 String displayname = name;
551 if (MediaFile.UNKNOWN_STRING.equals(name)) {
552 displayname = mUnknownAlbum;
554 vh.line1.setText(displayname);
556 name = cursor.getString(mArtistIdx);
558 if (MediaFile.UNKNOWN_STRING.equals(name)) {
559 displayname = mUnknownArtist;
561 StringBuilder builder = mStringBuilder;
562 builder.delete(0, builder.length());
563 builder.append(displayname);
564 builder.append(mAlbumSongSeparator);
566 int numsongs = cursor.getInt(mNumSongsIdx);
568 builder.append(context.getString(R.string.onesong));
570 final Object[] args = mFormatArgs;
572 builder.append(mResources.getQuantityString(R.plurals.Nsongs, numsongs, args));
574 vh.line2.setText(builder.toString());
576 ImageView iv = vh.icon;
577 // We don't actually need the path to the thumbnail file,
578 // we just use it to see if there is album art or not
579 String art = cursor.getString(mAlbumArtIndex);
580 if (art == null || art.length() == 0) {
581 iv.setImageDrawable(null);
583 int artIndex = cursor.getInt(0);
584 Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon);
585 iv.setImageDrawable(d);
588 int currentalbumid = MusicUtils.getCurrentAlbumId();
589 int aid = cursor.getInt(0);
590 iv = vh.play_indicator;
591 if (currentalbumid == aid) {
592 iv.setImageDrawable(mNowPlayingOverlay);
594 iv.setImageDrawable(null);
599 public void changeCursor(Cursor cursor) {
600 super.changeCursor(cursor);
601 mAlbumCursor = cursor;
602 mIndexer.setCursor(cursor);
606 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
607 mFilterString = constraint.toString();
608 return getAlbumCursor(null);
611 public Object[] getSections() {
612 return mIndexer.getSections();
615 public int getPositionForSection(int section) {
616 return mIndexer.getPositionForSection(section);
619 public int getSectionForPosition(int position) {
624 private Cursor mAlbumCursor;
625 private String mArtistId;
626 private boolean mHasHeader = false;