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.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.ServiceConnection;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.BitmapFactory;
33 import android.graphics.drawable.BitmapDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.media.AudioManager;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Message;
41 import android.provider.MediaStore;
42 import android.util.Log;
43 import android.view.ContextMenu;
44 import android.view.Menu;
45 import android.view.MenuItem;
46 import android.view.SubMenu;
47 import android.view.View;
48 import android.view.ViewGroup;
49 import android.view.Window;
50 import android.view.ContextMenu.ContextMenuInfo;
51 import android.widget.Adapter;
52 import android.widget.AlphabetIndexer;
53 import android.widget.CursorAdapter;
54 import android.widget.ExpandableListView;
55 import android.widget.ImageView;
56 import android.widget.ListAdapter;
57 import android.widget.ListView;
58 import android.widget.SectionIndexer;
59 import android.widget.SimpleCursorAdapter;
60 import android.widget.TextView;
61 import android.widget.AdapterView.AdapterContextMenuInfo;
63 import java.text.Collator;
65 public class AlbumBrowserActivity extends ListActivity
66 implements View.OnCreateContextMenuListener, MusicUtils.Defs, ServiceConnection
68 private String mCurrentAlbumId;
69 private String mCurrentAlbumName;
70 private String mCurrentArtistNameForAlbum;
71 boolean mIsUnknownArtist;
72 boolean mIsUnknownAlbum;
73 private AlbumListAdapter mAdapter;
74 private boolean mAdapterSent;
75 private final static int SEARCH = CHILD_MENU_BASE;
76 private static int mLastListPosCourse = -1;
77 private static int mLastListPosFine = -1;
79 public AlbumBrowserActivity()
83 /** Called when the activity is first created. */
85 public void onCreate(Bundle icicle)
88 mCurrentAlbumId = icicle.getString("selectedalbum");
89 mArtistId = icicle.getString("artist");
91 mArtistId = getIntent().getStringExtra("artist");
93 super.onCreate(icicle);
94 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
95 requestWindowFeature(Window.FEATURE_NO_TITLE);
96 setVolumeControlStream(AudioManager.STREAM_MUSIC);
97 MusicUtils.bindToService(this, this);
99 IntentFilter f = new IntentFilter();
100 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
101 f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
102 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
103 f.addDataScheme("file");
104 registerReceiver(mScanListener, f);
106 setContentView(R.layout.media_picker_activity);
107 MusicUtils.updateButtonBar(this, R.id.albumtab);
108 ListView lv = getListView();
109 lv.setOnCreateContextMenuListener(this);
110 lv.setTextFilterEnabled(true);
112 mAdapter = (AlbumListAdapter) getLastNonConfigurationInstance();
113 if (mAdapter == null) {
114 //Log.i("@@@", "starting query");
115 mAdapter = new AlbumListAdapter(
118 R.layout.track_list_item,
122 setListAdapter(mAdapter);
123 setTitle(R.string.working_albums);
124 getAlbumCursor(mAdapter.getQueryHandler(), null);
126 mAdapter.setActivity(this);
127 setListAdapter(mAdapter);
128 mAlbumCursor = mAdapter.getCursor();
129 if (mAlbumCursor != null) {
132 getAlbumCursor(mAdapter.getQueryHandler(), null);
138 public Object onRetainNonConfigurationInstance() {
144 public void onSaveInstanceState(Bundle outcicle) {
145 // need to store the selected item so we don't lose it in case
146 // of an orientation switch. Otherwise we could lose it while
147 // in the middle of specifying a playlist to add the item to.
148 outcicle.putString("selectedalbum", mCurrentAlbumId);
149 outcicle.putString("artist", mArtistId);
150 super.onSaveInstanceState(outcicle);
154 public void onDestroy() {
155 ListView lv = getListView();
157 mLastListPosCourse = lv.getFirstVisiblePosition();
158 View cv = lv.getChildAt(0);
160 mLastListPosFine = cv.getTop();
163 MusicUtils.unbindFromService(this);
164 // If we have an adapter and didn't send it off to another activity yet, we should
165 // close its cursor, which we do by assigning a null cursor to it. Doing this
166 // instead of closing the cursor directly keeps the framework from accessing
167 // the closed cursor later.
168 if (!mAdapterSent && mAdapter != null) {
169 mAdapter.changeCursor(null);
171 // Because we pass the adapter to the next activity, we need to make
172 // sure it doesn't keep a reference to this activity. We can do this
173 // by clearing its DatasetObservers, which setListAdapter(null) does.
174 setListAdapter(null);
176 unregisterReceiver(mScanListener);
181 public void onResume() {
183 IntentFilter f = new IntentFilter();
184 f.addAction(MediaPlaybackService.META_CHANGED);
185 f.addAction(MediaPlaybackService.QUEUE_CHANGED);
186 registerReceiver(mTrackListListener, f);
187 mTrackListListener.onReceive(null, null);
189 MusicUtils.setSpinnerState(this);
192 private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
194 public void onReceive(Context context, Intent intent) {
195 getListView().invalidateViews();
196 MusicUtils.updateNowPlaying(AlbumBrowserActivity.this);
199 private BroadcastReceiver mScanListener = new BroadcastReceiver() {
201 public void onReceive(Context context, Intent intent) {
202 MusicUtils.setSpinnerState(AlbumBrowserActivity.this);
203 mReScanHandler.sendEmptyMessage(0);
204 if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
205 MusicUtils.clearAlbumArtCache();
210 private Handler mReScanHandler = new Handler() {
212 public void handleMessage(Message msg) {
213 if (mAdapter != null) {
214 getAlbumCursor(mAdapter.getQueryHandler(), null);
220 public void onPause() {
221 unregisterReceiver(mTrackListListener);
222 mReScanHandler.removeCallbacksAndMessages(null);
226 public void init(Cursor c) {
228 if (mAdapter == null) {
231 mAdapter.changeCursor(c); // also sets mAlbumCursor
233 if (mAlbumCursor == null) {
234 MusicUtils.displayDatabaseError(this);
236 mReScanHandler.sendEmptyMessageDelayed(0, 1000);
240 // restore previous position
241 if (mLastListPosCourse >= 0) {
242 getListView().setSelectionFromTop(mLastListPosCourse, mLastListPosFine);
243 mLastListPosCourse = -1;
246 MusicUtils.hideDatabaseError(this);
247 MusicUtils.updateButtonBar(this, R.id.albumtab);
251 private void setTitle() {
252 CharSequence fancyName = "";
253 if (mAlbumCursor != null && mAlbumCursor.getCount() > 0) {
254 mAlbumCursor.moveToFirst();
255 fancyName = mAlbumCursor.getString(
256 mAlbumCursor.getColumnIndex(MediaStore.Audio.Albums.ARTIST));
257 if (fancyName == null || fancyName.equals(MediaStore.UNKNOWN_STRING))
258 fancyName = getText(R.string.unknown_artist_name);
261 if (mArtistId != null && fancyName != null)
264 setTitle(R.string.albums_title);
268 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
269 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
270 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
271 MusicUtils.makePlaylistMenu(this, sub);
272 menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
274 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
275 mAlbumCursor.moveToPosition(mi.position);
276 mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
277 mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
278 mCurrentArtistNameForAlbum = mAlbumCursor.getString(
279 mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
280 mIsUnknownArtist = mCurrentArtistNameForAlbum == null ||
281 mCurrentArtistNameForAlbum.equals(MediaStore.UNKNOWN_STRING);
282 mIsUnknownAlbum = mCurrentAlbumName == null ||
283 mCurrentAlbumName.equals(MediaStore.UNKNOWN_STRING);
284 if (mIsUnknownAlbum) {
285 menu.setHeaderTitle(getString(R.string.unknown_album_name));
287 menu.setHeaderTitle(mCurrentAlbumName);
289 if (!mIsUnknownAlbum || !mIsUnknownArtist) {
290 menu.add(0, SEARCH, 0, R.string.search_title);
295 public boolean onContextItemSelected(MenuItem item) {
296 switch (item.getItemId()) {
297 case PLAY_SELECTION: {
298 // play the selected album
299 long [] list = MusicUtils.getSongListForAlbum(this, Long.parseLong(mCurrentAlbumId));
300 MusicUtils.playAll(this, list, 0);
305 long [] list = MusicUtils.getSongListForAlbum(this, Long.parseLong(mCurrentAlbumId));
306 MusicUtils.addToCurrentPlaylist(this, list);
311 Intent intent = new Intent();
312 intent.setClass(this, CreatePlaylist.class);
313 startActivityForResult(intent, NEW_PLAYLIST);
317 case PLAYLIST_SELECTED: {
318 long [] list = MusicUtils.getSongListForAlbum(this, Long.parseLong(mCurrentAlbumId));
319 long playlist = item.getIntent().getLongExtra("playlist", 0);
320 MusicUtils.addToPlaylist(this, list, playlist);
324 long [] list = MusicUtils.getSongListForAlbum(this, Long.parseLong(mCurrentAlbumId));
325 String f = getString(R.string.delete_album_desc);
326 String desc = String.format(f, mCurrentAlbumName);
327 Bundle b = new Bundle();
328 b.putString("description", desc);
329 b.putLongArray("items", list);
330 Intent intent = new Intent();
331 intent.setClass(this, DeleteItems.class);
333 startActivityForResult(intent, -1);
341 return super.onContextItemSelected(item);
345 CharSequence title = null;
348 Intent i = new Intent();
349 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
350 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
353 if (!mIsUnknownAlbum) {
354 query = mCurrentAlbumName;
355 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
356 title = mCurrentAlbumName;
358 if(!mIsUnknownArtist) {
359 query = query + " " + mCurrentArtistNameForAlbum;
360 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
361 title = title + " " + mCurrentArtistNameForAlbum;
363 // Since we hide the 'search' menu item when both album and artist are
364 // unknown, the query and title strings will have at least one of those.
365 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
366 title = getString(R.string.mediasearch, title);
367 i.putExtra(SearchManager.QUERY, query);
369 startActivity(Intent.createChooser(i, title));
373 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
374 switch (requestCode) {
376 if (resultCode == RESULT_CANCELED) {
379 getAlbumCursor(mAdapter.getQueryHandler(), null);
384 if (resultCode == RESULT_OK) {
385 Uri uri = intent.getData();
387 long [] list = MusicUtils.getSongListForAlbum(this, Long.parseLong(mCurrentAlbumId));
388 MusicUtils.addToPlaylist(this, list, Long.parseLong(uri.getLastPathSegment()));
396 protected void onListItemClick(ListView l, View v, int position, long id)
398 Intent intent = new Intent(Intent.ACTION_PICK);
399 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
400 intent.putExtra("album", Long.valueOf(id).toString());
401 intent.putExtra("artist", mArtistId);
402 startActivity(intent);
406 public boolean onCreateOptionsMenu(Menu menu) {
407 super.onCreateOptionsMenu(menu);
408 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
409 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
414 public boolean onPrepareOptionsMenu(Menu menu) {
415 MusicUtils.setPartyShuffleMenuIcon(menu);
416 return super.onPrepareOptionsMenu(menu);
420 public boolean onOptionsItemSelected(MenuItem item) {
423 switch (item.getItemId()) {
425 MusicUtils.togglePartyShuffle();
429 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
430 new String [] { MediaStore.Audio.Media._ID},
431 MediaStore.Audio.Media.IS_MUSIC + "=1", null,
432 MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
433 if (cursor != null) {
434 MusicUtils.shuffleAll(this, cursor);
439 return super.onOptionsItemSelected(item);
442 private Cursor getAlbumCursor(AsyncQueryHandler async, String filter) {
443 StringBuilder where = new StringBuilder();
444 where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
446 // Add in the filtering constraints
447 String [] keywords = null;
448 if (filter != null) {
449 String [] searchWords = filter.split(" ");
450 keywords = new String[searchWords.length];
451 Collator col = Collator.getInstance();
452 col.setStrength(Collator.PRIMARY);
453 for (int i = 0; i < searchWords.length; i++) {
454 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
456 for (int i = 0; i < searchWords.length; i++) {
457 where.append(" AND ");
458 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
459 where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?");
463 String whereclause = where.toString();
465 String[] cols = new String[] {
466 MediaStore.Audio.Albums._ID,
467 MediaStore.Audio.Albums.ARTIST,
468 MediaStore.Audio.Albums.ALBUM,
469 MediaStore.Audio.Albums.ALBUM_ART
472 if (mArtistId != null) {
474 async.startQuery(0, null,
475 MediaStore.Audio.Artists.Albums.getContentUri("external",
476 Long.valueOf(mArtistId)),
477 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
479 ret = MusicUtils.query(this,
480 MediaStore.Audio.Artists.Albums.getContentUri("external",
481 Long.valueOf(mArtistId)),
482 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
486 async.startQuery(0, null,
487 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
488 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
490 ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
491 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
497 static class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer {
499 private final Drawable mNowPlayingOverlay;
500 private final BitmapDrawable mDefaultAlbumIcon;
501 private int mAlbumIdx;
502 private int mArtistIdx;
503 private int mAlbumArtIndex;
504 private final Resources mResources;
505 private final StringBuilder mStringBuilder = new StringBuilder();
506 private final String mUnknownAlbum;
507 private final String mUnknownArtist;
508 private final String mAlbumSongSeparator;
509 private final Object[] mFormatArgs = new Object[1];
510 private AlphabetIndexer mIndexer;
511 private AlbumBrowserActivity mActivity;
512 private AsyncQueryHandler mQueryHandler;
513 private String mConstraint = null;
514 private boolean mConstraintIsValid = false;
516 static class ViewHolder {
519 ImageView play_indicator;
523 class QueryHandler extends AsyncQueryHandler {
524 QueryHandler(ContentResolver res) {
529 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
530 //Log.i("@@@", "query complete");
531 mActivity.init(cursor);
535 AlbumListAdapter(Context context, AlbumBrowserActivity currentactivity,
536 int layout, Cursor cursor, String[] from, int[] to) {
537 super(context, layout, cursor, from, to);
539 mActivity = currentactivity;
540 mQueryHandler = new QueryHandler(context.getContentResolver());
542 mUnknownAlbum = context.getString(R.string.unknown_album_name);
543 mUnknownArtist = context.getString(R.string.unknown_artist_name);
544 mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
546 Resources r = context.getResources();
547 mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
549 Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list);
550 mDefaultAlbumIcon = new BitmapDrawable(context.getResources(), b);
551 // no filter or dither, it's a lot faster and we can't tell the difference
552 mDefaultAlbumIcon.setFilterBitmap(false);
553 mDefaultAlbumIcon.setDither(false);
554 getColumnIndices(cursor);
555 mResources = context.getResources();
558 private void getColumnIndices(Cursor cursor) {
559 if (cursor != null) {
560 mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
561 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST);
562 mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART);
564 if (mIndexer != null) {
565 mIndexer.setCursor(cursor);
567 mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString(
568 R.string.fast_scroll_alphabet));
573 public void setActivity(AlbumBrowserActivity newactivity) {
574 mActivity = newactivity;
577 public AsyncQueryHandler getQueryHandler() {
578 return mQueryHandler;
582 public View newView(Context context, Cursor cursor, ViewGroup parent) {
583 View v = super.newView(context, cursor, parent);
584 ViewHolder vh = new ViewHolder();
585 vh.line1 = (TextView) v.findViewById(R.id.line1);
586 vh.line2 = (TextView) v.findViewById(R.id.line2);
587 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
588 vh.icon = (ImageView) v.findViewById(R.id.icon);
589 vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
590 vh.icon.setPadding(0, 0, 1, 0);
596 public void bindView(View view, Context context, Cursor cursor) {
598 ViewHolder vh = (ViewHolder) view.getTag();
600 String name = cursor.getString(mAlbumIdx);
601 String displayname = name;
602 boolean unknown = name == null || name.equals(MediaStore.UNKNOWN_STRING);
604 displayname = mUnknownAlbum;
606 vh.line1.setText(displayname);
608 name = cursor.getString(mArtistIdx);
610 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
611 displayname = mUnknownArtist;
613 vh.line2.setText(displayname);
615 ImageView iv = vh.icon;
616 // We don't actually need the path to the thumbnail file,
617 // we just use it to see if there is album art or not
618 String art = cursor.getString(mAlbumArtIndex);
619 long aid = cursor.getLong(0);
620 if (unknown || art == null || art.length() == 0) {
621 iv.setImageDrawable(null);
623 Drawable d = MusicUtils.getCachedArtwork(context, aid, mDefaultAlbumIcon);
624 iv.setImageDrawable(d);
627 long currentalbumid = MusicUtils.getCurrentAlbumId();
628 iv = vh.play_indicator;
629 if (currentalbumid == aid) {
630 iv.setImageDrawable(mNowPlayingOverlay);
632 iv.setImageDrawable(null);
637 public void changeCursor(Cursor cursor) {
638 if (mActivity.isFinishing() && cursor != null) {
642 if (cursor != mActivity.mAlbumCursor) {
643 mActivity.mAlbumCursor = cursor;
644 getColumnIndices(cursor);
645 super.changeCursor(cursor);
650 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
651 String s = constraint.toString();
652 if (mConstraintIsValid && (
653 (s == null && mConstraint == null) ||
654 (s != null && s.equals(mConstraint)))) {
657 Cursor c = mActivity.getAlbumCursor(null, s);
659 mConstraintIsValid = true;
663 public Object[] getSections() {
664 return mIndexer.getSections();
667 public int getPositionForSection(int section) {
668 return mIndexer.getPositionForSection(section);
671 public int getSectionForPosition(int position) {
676 private Cursor mAlbumCursor;
677 private String mArtistId;
679 public void onServiceConnected(ComponentName name, IBinder service) {
680 MusicUtils.updateNowPlaying(this);
683 public void onServiceDisconnected(ComponentName name) {