AlbumBrowserActivity.java revision 539844af4c430cf8d1f138d1482520bc4cd3847d
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 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 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 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. 15 */ 16 17package com.android.music; 18 19import android.app.ListActivity; 20import android.app.SearchManager; 21import android.content.AsyncQueryHandler; 22import android.content.BroadcastReceiver; 23import android.content.ContentResolver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.content.res.Resources; 28import android.database.Cursor; 29import android.graphics.Bitmap; 30import android.graphics.BitmapFactory; 31import android.graphics.drawable.BitmapDrawable; 32import android.graphics.drawable.Drawable; 33import android.media.AudioManager; 34import android.media.MediaFile; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.Handler; 38import android.os.Message; 39import android.provider.MediaStore; 40import android.view.ContextMenu; 41import android.view.Menu; 42import android.view.MenuItem; 43import android.view.SubMenu; 44import android.view.View; 45import android.view.ViewGroup; 46import android.view.Window; 47import android.view.ContextMenu.ContextMenuInfo; 48import android.widget.Adapter; 49import android.widget.AlphabetIndexer; 50import android.widget.CursorAdapter; 51import android.widget.ImageView; 52import android.widget.ListAdapter; 53import android.widget.ListView; 54import android.widget.SectionIndexer; 55import android.widget.SimpleCursorAdapter; 56import android.widget.TextView; 57import android.widget.AdapterView.AdapterContextMenuInfo; 58 59import java.text.Collator; 60 61public class AlbumBrowserActivity extends ListActivity 62 implements View.OnCreateContextMenuListener, MusicUtils.Defs 63{ 64 private String mCurrentAlbumId; 65 private String mCurrentAlbumName; 66 private String mCurrentArtistNameForAlbum; 67 private AlbumListAdapter mAdapter; 68 private boolean mAdapterSent; 69 private final static int SEARCH = CHILD_MENU_BASE; 70 71 public AlbumBrowserActivity() 72 { 73 } 74 75 /** Called when the activity is first created. */ 76 @Override 77 public void onCreate(Bundle icicle) 78 { 79 if (icicle != null) { 80 mCurrentAlbumId = icicle.getString("selectedalbum"); 81 mArtistId = icicle.getString("artist"); 82 } else { 83 mArtistId = getIntent().getStringExtra("artist"); 84 } 85 super.onCreate(icicle); 86 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 87 setVolumeControlStream(AudioManager.STREAM_MUSIC); 88 MusicUtils.bindToService(this); 89 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); 96 97 setContentView(R.layout.media_picker_activity); 98 ListView lv = getListView(); 99 lv.setFastScrollEnabled(true); 100 lv.setOnCreateContextMenuListener(this); 101 lv.setTextFilterEnabled(true); 102 103 mAdapter = (AlbumListAdapter) getLastNonConfigurationInstance(); 104 if (mAdapter == null) { 105 //Log.i("@@@", "starting query"); 106 mAdapter = new AlbumListAdapter( 107 getApplication(), 108 this, 109 R.layout.track_list_item, 110 mAlbumCursor, 111 new String[] {}, 112 new int[] {}); 113 setListAdapter(mAdapter); 114 setTitle(R.string.working_albums); 115 getAlbumCursor(mAdapter.getQueryHandler(), null); 116 } else { 117 mAdapter.setActivity(this); 118 setListAdapter(mAdapter); 119 mAlbumCursor = mAdapter.getCursor(); 120 if (mAlbumCursor != null) { 121 init(mAlbumCursor); 122 } else { 123 getAlbumCursor(mAdapter.getQueryHandler(), null); 124 } 125 } 126 } 127 128 @Override 129 public Object onRetainNonConfigurationInstance() { 130 mAdapterSent = true; 131 return mAdapter; 132 } 133 134 @Override 135 public void onSaveInstanceState(Bundle outcicle) { 136 // need to store the selected item so we don't lose it in case 137 // of an orientation switch. Otherwise we could lose it while 138 // in the middle of specifying a playlist to add the item to. 139 outcicle.putString("selectedalbum", mCurrentAlbumId); 140 outcicle.putString("artist", mArtistId); 141 super.onSaveInstanceState(outcicle); 142 } 143 144 @Override 145 public void onDestroy() { 146 MusicUtils.unbindFromService(this); 147 if (!mAdapterSent) { 148 Cursor c = mAdapter.getCursor(); 149 if (c != null) { 150 c.close(); 151 } 152 } 153 unregisterReceiver(mScanListener); 154 super.onDestroy(); 155 } 156 157 @Override 158 public void onResume() { 159 super.onResume(); 160 IntentFilter f = new IntentFilter(); 161 f.addAction(MediaPlaybackService.META_CHANGED); 162 f.addAction(MediaPlaybackService.QUEUE_CHANGED); 163 registerReceiver(mTrackListListener, f); 164 mTrackListListener.onReceive(null, null); 165 166 MusicUtils.setSpinnerState(this); 167 } 168 169 private BroadcastReceiver mTrackListListener = new BroadcastReceiver() { 170 @Override 171 public void onReceive(Context context, Intent intent) { 172 getListView().invalidateViews(); 173 } 174 }; 175 private BroadcastReceiver mScanListener = new BroadcastReceiver() { 176 @Override 177 public void onReceive(Context context, Intent intent) { 178 MusicUtils.setSpinnerState(AlbumBrowserActivity.this); 179 mReScanHandler.sendEmptyMessage(0); 180 if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) { 181 MusicUtils.clearAlbumArtCache(); 182 } 183 } 184 }; 185 186 private Handler mReScanHandler = new Handler() { 187 @Override 188 public void handleMessage(Message msg) { 189 getAlbumCursor(mAdapter.getQueryHandler(), null); 190 } 191 }; 192 193 @Override 194 public void onPause() { 195 unregisterReceiver(mTrackListListener); 196 mReScanHandler.removeCallbacksAndMessages(null); 197 super.onPause(); 198 } 199 200 public void init(Cursor c) { 201 202 mAdapter.changeCursor(c); // also sets mAlbumCursor 203 204 if (mAlbumCursor == null) { 205 MusicUtils.displayDatabaseError(this); 206 closeContextMenu(); 207 mReScanHandler.sendEmptyMessageDelayed(0, 1000); 208 return; 209 } 210 211 MusicUtils.hideDatabaseError(this); 212 setTitle(); 213 } 214 215 private void setTitle() { 216 CharSequence fancyName = ""; 217 if (mAlbumCursor != null && mAlbumCursor.getCount() > 0) { 218 mAlbumCursor.moveToFirst(); 219 fancyName = mAlbumCursor.getString(3); 220 if (MediaFile.UNKNOWN_STRING.equals(fancyName)) 221 fancyName = getText(R.string.unknown_artist_name); 222 } 223 224 if (mArtistId != null && fancyName != null) 225 setTitle(fancyName); 226 else 227 setTitle(R.string.albums_title); 228 } 229 230 @Override 231 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 232 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection); 233 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist); 234 MusicUtils.makePlaylistMenu(this, sub); 235 menu.add(0, DELETE_ITEM, 0, R.string.delete_item); 236 menu.add(0, SEARCH, 0, R.string.search_title); 237 238 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn; 239 mAlbumCursor.moveToPosition(mi.position); 240 mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID)); 241 mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)); 242 mCurrentArtistNameForAlbum = mAlbumCursor.getString( 243 mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST)); 244 menu.setHeaderTitle(mCurrentAlbumName); 245 } 246 247 @Override 248 public boolean onContextItemSelected(MenuItem item) { 249 switch (item.getItemId()) { 250 case PLAY_SELECTION: { 251 // play the selected album 252 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId)); 253 MusicUtils.playAll(this, list, 0); 254 return true; 255 } 256 257 case QUEUE: { 258 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId)); 259 MusicUtils.addToCurrentPlaylist(this, list); 260 return true; 261 } 262 263 case NEW_PLAYLIST: { 264 Intent intent = new Intent(); 265 intent.setClass(this, CreatePlaylist.class); 266 startActivityForResult(intent, NEW_PLAYLIST); 267 return true; 268 } 269 270 case PLAYLIST_SELECTED: { 271 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId)); 272 int playlist = item.getIntent().getIntExtra("playlist", 0); 273 MusicUtils.addToPlaylist(this, list, playlist); 274 return true; 275 } 276 case DELETE_ITEM: { 277 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId)); 278 String f = getString(R.string.delete_album_desc); 279 String desc = String.format(f, mCurrentAlbumName); 280 Bundle b = new Bundle(); 281 b.putString("description", desc); 282 b.putIntArray("items", list); 283 Intent intent = new Intent(); 284 intent.setClass(this, DeleteItems.class); 285 intent.putExtras(b); 286 startActivityForResult(intent, -1); 287 return true; 288 } 289 case SEARCH: 290 doSearch(); 291 return true; 292 293 } 294 return super.onContextItemSelected(item); 295 } 296 297 void doSearch() { 298 CharSequence title = null; 299 String query = null; 300 301 Intent i = new Intent(); 302 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH); 303 304 title = mCurrentAlbumName; 305 query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName; 306 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum); 307 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName); 308 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE); 309 title = getString(R.string.mediasearch, title); 310 i.putExtra(SearchManager.QUERY, query); 311 312 startActivity(Intent.createChooser(i, title)); 313 } 314 315 @Override 316 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 317 switch (requestCode) { 318 case SCAN_DONE: 319 if (resultCode == RESULT_CANCELED) { 320 finish(); 321 } else { 322 getAlbumCursor(mAdapter.getQueryHandler(), null); 323 } 324 break; 325 326 case NEW_PLAYLIST: 327 if (resultCode == RESULT_OK) { 328 Uri uri = intent.getData(); 329 if (uri != null) { 330 int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId)); 331 MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment())); 332 } 333 } 334 break; 335 } 336 } 337 338 @Override 339 protected void onListItemClick(ListView l, View v, int position, long id) 340 { 341 if (mHasHeader) { 342 position --; 343 } 344 Intent intent = new Intent(Intent.ACTION_PICK); 345 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 346 intent.putExtra("album", Long.valueOf(id).toString()); 347 intent.putExtra("artist", mArtistId); 348 startActivity(intent); 349 } 350 351 @Override 352 public boolean onCreateOptionsMenu(Menu menu) { 353 super.onCreateOptionsMenu(menu); 354 menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library); 355 menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback); 356 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle); 357 return true; 358 } 359 360 @Override 361 public boolean onPrepareOptionsMenu(Menu menu) { 362 menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded()); 363 return super.onPrepareOptionsMenu(menu); 364 } 365 366 @Override 367 public boolean onOptionsItemSelected(MenuItem item) { 368 Intent intent; 369 Cursor cursor; 370 switch (item.getItemId()) { 371 case GOTO_START: 372 intent = new Intent(); 373 intent.setClass(this, MusicBrowserActivity.class); 374 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 375 startActivity(intent); 376 return true; 377 378 case GOTO_PLAYBACK: 379 intent = new Intent("com.android.music.PLAYBACK_VIEWER"); 380 startActivity(intent); 381 return true; 382 383 case SHUFFLE_ALL: 384 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 385 new String [] { MediaStore.Audio.Media._ID}, 386 MediaStore.Audio.Media.IS_MUSIC + "=1", null, 387 MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 388 if (cursor != null) { 389 MusicUtils.shuffleAll(this, cursor); 390 cursor.close(); 391 } 392 return true; 393 } 394 return super.onOptionsItemSelected(item); 395 } 396 397 private Cursor getAlbumCursor(AsyncQueryHandler async, String filter) { 398 StringBuilder where = new StringBuilder(); 399 where.append(MediaStore.Audio.Albums.ALBUM + " != ''"); 400 401 // Add in the filtering constraints 402 String [] keywords = null; 403 if (filter != null) { 404 String [] searchWords = filter.split(" "); 405 keywords = new String[searchWords.length]; 406 Collator col = Collator.getInstance(); 407 col.setStrength(Collator.PRIMARY); 408 for (int i = 0; i < searchWords.length; i++) { 409 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 410 } 411 for (int i = 0; i < searchWords.length; i++) { 412 where.append(" AND "); 413 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||"); 414 where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?"); 415 } 416 } 417 418 String whereclause = where.toString(); 419 420 String[] cols = new String[] { 421 MediaStore.Audio.Albums._ID, 422 MediaStore.Audio.Albums.ALBUM, 423 MediaStore.Audio.Albums.ALBUM_KEY, 424 MediaStore.Audio.Albums.ARTIST, 425 MediaStore.Audio.Albums.NUMBER_OF_SONGS, 426 MediaStore.Audio.Albums.ALBUM_ART 427 }; 428 Cursor ret = null; 429 if (mArtistId != null) { 430 if (async != null) { 431 async.startQuery(0, null, 432 MediaStore.Audio.Artists.Albums.getContentUri("external", 433 Long.valueOf(mArtistId)), 434 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER); 435 } else { 436 ret = MusicUtils.query(this, 437 MediaStore.Audio.Artists.Albums.getContentUri("external", 438 Long.valueOf(mArtistId)), 439 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER); 440 } 441 } else { 442 if (async != null) { 443 async.startQuery(0, null, 444 MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 445 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER); 446 } else { 447 ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, 448 cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER); 449 } 450 } 451 return ret; 452 } 453 454 static class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer { 455 456 private final Drawable mNowPlayingOverlay; 457 private final BitmapDrawable mDefaultAlbumIcon; 458 private int mAlbumIdx; 459 private int mArtistIdx; 460 private int mNumSongsIdx; 461 private int mAlbumArtIndex; 462 private final Resources mResources; 463 private final StringBuilder mStringBuilder = new StringBuilder(); 464 private final String mUnknownAlbum; 465 private final String mUnknownArtist; 466 private final String mAlbumSongSeparator; 467 private final Object[] mFormatArgs = new Object[1]; 468 private AlphabetIndexer mIndexer; 469 private AlbumBrowserActivity mActivity; 470 private AsyncQueryHandler mQueryHandler; 471 472 class ViewHolder { 473 TextView line1; 474 TextView line2; 475 TextView duration; 476 ImageView play_indicator; 477 ImageView icon; 478 } 479 480 class QueryHandler extends AsyncQueryHandler { 481 QueryHandler(ContentResolver res) { 482 super(res); 483 } 484 485 @Override 486 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 487 //Log.i("@@@", "query complete"); 488 mActivity.init(cursor); 489 } 490 } 491 492 AlbumListAdapter(Context context, AlbumBrowserActivity currentactivity, 493 int layout, Cursor cursor, String[] from, int[] to) { 494 super(context, layout, cursor, from, to); 495 496 mActivity = currentactivity; 497 mQueryHandler = new QueryHandler(context.getContentResolver()); 498 499 mUnknownAlbum = context.getString(R.string.unknown_album_name); 500 mUnknownArtist = context.getString(R.string.unknown_artist_name); 501 mAlbumSongSeparator = context.getString(R.string.albumsongseparator); 502 503 Resources r = context.getResources(); 504 mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list); 505 506 Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list); 507 mDefaultAlbumIcon = new BitmapDrawable(b); 508 // no filter or dither, it's a lot faster and we can't tell the difference 509 mDefaultAlbumIcon.setFilterBitmap(false); 510 mDefaultAlbumIcon.setDither(false); 511 getColumnIndices(cursor); 512 mResources = context.getResources(); 513 } 514 515 private void getColumnIndices(Cursor cursor) { 516 if (cursor != null) { 517 mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM); 518 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST); 519 mNumSongsIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS); 520 mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART); 521 522 if (mIndexer != null) { 523 mIndexer.setCursor(cursor); 524 } else { 525 mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString( 526 com.android.internal.R.string.fast_scroll_alphabet)); 527 } 528 } 529 } 530 531 public void setActivity(AlbumBrowserActivity newactivity) { 532 mActivity = newactivity; 533 } 534 535 public AsyncQueryHandler getQueryHandler() { 536 return mQueryHandler; 537 } 538 539 @Override 540 public View newView(Context context, Cursor cursor, ViewGroup parent) { 541 View v = super.newView(context, cursor, parent); 542 ViewHolder vh = new ViewHolder(); 543 vh.line1 = (TextView) v.findViewById(R.id.line1); 544 vh.line2 = (TextView) v.findViewById(R.id.line2); 545 vh.duration = (TextView) v.findViewById(R.id.duration); 546 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator); 547 vh.icon = (ImageView) v.findViewById(R.id.icon); 548 vh.icon.setBackgroundDrawable(mDefaultAlbumIcon); 549 vh.icon.setPadding(0, 0, 1, 0); 550 v.setTag(vh); 551 return v; 552 } 553 554 @Override 555 public void bindView(View view, Context context, Cursor cursor) { 556 557 ViewHolder vh = (ViewHolder) view.getTag(); 558 559 String name = cursor.getString(mAlbumIdx); 560 String displayname = name; 561 boolean unknown = name.equals(MediaFile.UNKNOWN_STRING); 562 if (unknown) { 563 displayname = mUnknownAlbum; 564 } 565 vh.line1.setText(displayname); 566 567 name = cursor.getString(mArtistIdx); 568 displayname = name; 569 if (MediaFile.UNKNOWN_STRING.equals(name)) { 570 displayname = mUnknownArtist; 571 } 572 vh.line2.setText(displayname); 573 574 ImageView iv = vh.icon; 575 // We don't actually need the path to the thumbnail file, 576 // we just use it to see if there is album art or not 577 String art = cursor.getString(mAlbumArtIndex); 578 if (unknown || art == null || art.length() == 0) { 579 iv.setImageDrawable(null); 580 } else { 581 int artIndex = cursor.getInt(0); 582 Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon); 583 iv.setImageDrawable(d); 584 } 585 586 int currentalbumid = MusicUtils.getCurrentAlbumId(); 587 int aid = cursor.getInt(0); 588 iv = vh.play_indicator; 589 if (currentalbumid == aid) { 590 iv.setImageDrawable(mNowPlayingOverlay); 591 } else { 592 iv.setImageDrawable(null); 593 } 594 } 595 596 @Override 597 public void changeCursor(Cursor cursor) { 598 if (cursor != mActivity.mAlbumCursor) { 599 mActivity.mAlbumCursor = cursor; 600 getColumnIndices(cursor); 601 super.changeCursor(cursor); 602 } 603 } 604 605 @Override 606 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 607 return mActivity.getAlbumCursor(null, constraint.toString()); 608 } 609 610 public Object[] getSections() { 611 return mIndexer.getSections(); 612 } 613 614 public int getPositionForSection(int section) { 615 return mIndexer.getPositionForSection(section); 616 } 617 618 public int getSectionForPosition(int position) { 619 return 0; 620 } 621 } 622 623 private Cursor mAlbumCursor; 624 private String mArtistId; 625 private boolean mHasHeader = false; 626} 627 628