TrackBrowserActivity.java revision 89f6c66505717689d9ea3f3d8e81709616b3e588
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 com.android.music.MusicUtils.ServiceToken; 20 21import android.app.ListActivity; 22import android.app.SearchManager; 23import android.content.AsyncQueryHandler; 24import android.content.BroadcastReceiver; 25import android.content.ComponentName; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.Intent; 31import android.content.IntentFilter; 32import android.content.ServiceConnection; 33import android.database.AbstractCursor; 34import android.database.CharArrayBuffer; 35import android.database.Cursor; 36import android.graphics.Bitmap; 37import android.media.AudioManager; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.os.IBinder; 42import android.os.Message; 43import android.os.RemoteException; 44import android.provider.MediaStore; 45import android.provider.MediaStore.Audio.Playlists; 46import android.text.TextUtils; 47import android.util.Log; 48import android.view.ContextMenu; 49import android.view.KeyEvent; 50import android.view.Menu; 51import android.view.MenuItem; 52import android.view.SubMenu; 53import android.view.View; 54import android.view.ViewGroup; 55import android.view.Window; 56import android.view.ContextMenu.ContextMenuInfo; 57import android.widget.AlphabetIndexer; 58import android.widget.ImageView; 59import android.widget.ListView; 60import android.widget.SectionIndexer; 61import android.widget.SimpleCursorAdapter; 62import android.widget.TextView; 63import android.widget.AdapterView.AdapterContextMenuInfo; 64 65import java.text.Collator; 66import java.util.Arrays; 67 68public class TrackBrowserActivity extends ListActivity 69 implements View.OnCreateContextMenuListener, MusicUtils.Defs, ServiceConnection 70{ 71 private static final int Q_SELECTED = CHILD_MENU_BASE; 72 private static final int Q_ALL = CHILD_MENU_BASE + 1; 73 private static final int SAVE_AS_PLAYLIST = CHILD_MENU_BASE + 2; 74 private static final int PLAY_ALL = CHILD_MENU_BASE + 3; 75 private static final int CLEAR_PLAYLIST = CHILD_MENU_BASE + 4; 76 private static final int REMOVE = CHILD_MENU_BASE + 5; 77 private static final int SEARCH = CHILD_MENU_BASE + 6; 78 79 80 private static final String LOGTAG = "TrackBrowser"; 81 82 private String[] mCursorCols; 83 private String[] mPlaylistMemberCols; 84 private boolean mDeletedOneRow = false; 85 private boolean mEditMode = false; 86 private String mCurrentTrackName; 87 private String mCurrentAlbumName; 88 private String mCurrentArtistNameForAlbum; 89 private ListView mTrackList; 90 private Cursor mTrackCursor; 91 private TrackListAdapter mAdapter; 92 private boolean mAdapterSent = false; 93 private String mAlbumId; 94 private String mArtistId; 95 private String mPlaylist; 96 private String mGenre; 97 private String mSortOrder; 98 private int mSelectedPosition; 99 private long mSelectedId; 100 private static int mLastListPosCourse = -1; 101 private static int mLastListPosFine = -1; 102 private boolean mUseLastListPos = false; 103 private ServiceToken mToken; 104 105 public TrackBrowserActivity() 106 { 107 } 108 109 /** Called when the activity is first created. */ 110 @Override 111 public void onCreate(Bundle icicle) 112 { 113 super.onCreate(icicle); 114 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 115 Intent intent = getIntent(); 116 if (intent != null) { 117 if (intent.getBooleanExtra("withtabs", false)) { 118 requestWindowFeature(Window.FEATURE_NO_TITLE); 119 } 120 } 121 setVolumeControlStream(AudioManager.STREAM_MUSIC); 122 if (icicle != null) { 123 mSelectedId = icicle.getLong("selectedtrack"); 124 mAlbumId = icicle.getString("album"); 125 mArtistId = icicle.getString("artist"); 126 mPlaylist = icicle.getString("playlist"); 127 mGenre = icicle.getString("genre"); 128 mEditMode = icicle.getBoolean("editmode", false); 129 } else { 130 mAlbumId = intent.getStringExtra("album"); 131 // If we have an album, show everything on the album, not just stuff 132 // by a particular artist. 133 mArtistId = intent.getStringExtra("artist"); 134 mPlaylist = intent.getStringExtra("playlist"); 135 mGenre = intent.getStringExtra("genre"); 136 mEditMode = intent.getAction().equals(Intent.ACTION_EDIT); 137 } 138 139 mCursorCols = new String[] { 140 MediaStore.Audio.Media._ID, 141 MediaStore.Audio.Media.TITLE, 142 MediaStore.Audio.Media.DATA, 143 MediaStore.Audio.Media.ALBUM, 144 MediaStore.Audio.Media.ARTIST, 145 MediaStore.Audio.Media.ARTIST_ID, 146 MediaStore.Audio.Media.DURATION 147 }; 148 mPlaylistMemberCols = new String[] { 149 MediaStore.Audio.Playlists.Members._ID, 150 MediaStore.Audio.Media.TITLE, 151 MediaStore.Audio.Media.DATA, 152 MediaStore.Audio.Media.ALBUM, 153 MediaStore.Audio.Media.ARTIST, 154 MediaStore.Audio.Media.ARTIST_ID, 155 MediaStore.Audio.Media.DURATION, 156 MediaStore.Audio.Playlists.Members.PLAY_ORDER, 157 MediaStore.Audio.Playlists.Members.AUDIO_ID, 158 MediaStore.Audio.Media.IS_MUSIC 159 }; 160 161 setContentView(R.layout.media_picker_activity); 162 mUseLastListPos = MusicUtils.updateButtonBar(this, R.id.songtab); 163 mTrackList = getListView(); 164 mTrackList.setOnCreateContextMenuListener(this); 165 if (mEditMode) { 166 ((TouchInterceptor) mTrackList).setDropListener(mDropListener); 167 ((TouchInterceptor) mTrackList).setRemoveListener(mRemoveListener); 168 mTrackList.setCacheColorHint(0); 169 } else { 170 mTrackList.setTextFilterEnabled(true); 171 } 172 mAdapter = (TrackListAdapter) getLastNonConfigurationInstance(); 173 174 if (mAdapter != null) { 175 mAdapter.setActivity(this); 176 setListAdapter(mAdapter); 177 } 178 mToken = MusicUtils.bindToService(this, this); 179 180 // don't set the album art until after the view has been layed out 181 mTrackList.post(new Runnable() { 182 183 public void run() { 184 setAlbumArtBackground(); 185 } 186 }); 187 } 188 189 public void onServiceConnected(ComponentName name, IBinder service) 190 { 191 IntentFilter f = new IntentFilter(); 192 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 193 f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 194 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 195 f.addDataScheme("file"); 196 registerReceiver(mScanListener, f); 197 198 if (mAdapter == null) { 199 //Log.i("@@@", "starting query"); 200 mAdapter = new TrackListAdapter( 201 getApplication(), // need to use application context to avoid leaks 202 this, 203 mEditMode ? R.layout.edit_track_list_item : R.layout.track_list_item, 204 null, // cursor 205 new String[] {}, 206 new int[] {}, 207 "nowplaying".equals(mPlaylist), 208 mPlaylist != null && 209 !(mPlaylist.equals("podcasts") || mPlaylist.equals("recentlyadded"))); 210 setListAdapter(mAdapter); 211 setTitle(R.string.working_songs); 212 getTrackCursor(mAdapter.getQueryHandler(), null, true); 213 } else { 214 mTrackCursor = mAdapter.getCursor(); 215 // If mTrackCursor is null, this can be because it doesn't have 216 // a cursor yet (because the initial query that sets its cursor 217 // is still in progress), or because the query failed. 218 // In order to not flash the error dialog at the user for the 219 // first case, simply retry the query when the cursor is null. 220 // Worst case, we end up doing the same query twice. 221 if (mTrackCursor != null) { 222 init(mTrackCursor, false); 223 } else { 224 setTitle(R.string.working_songs); 225 getTrackCursor(mAdapter.getQueryHandler(), null, true); 226 } 227 } 228 if (!mEditMode) { 229 MusicUtils.updateNowPlaying(this); 230 } 231 } 232 233 public void onServiceDisconnected(ComponentName name) { 234 // we can't really function without the service, so don't 235 finish(); 236 } 237 238 @Override 239 public Object onRetainNonConfigurationInstance() { 240 TrackListAdapter a = mAdapter; 241 mAdapterSent = true; 242 return a; 243 } 244 245 @Override 246 public void onDestroy() { 247 ListView lv = getListView(); 248 if (lv != null) { 249 if (mUseLastListPos) { 250 mLastListPosCourse = lv.getFirstVisiblePosition(); 251 View cv = lv.getChildAt(0); 252 if (cv != null) { 253 mLastListPosFine = cv.getTop(); 254 } 255 } 256 if (mEditMode) { 257 // clear the listeners so we won't get any more callbacks 258 ((TouchInterceptor) lv).setDropListener(null); 259 ((TouchInterceptor) lv).setRemoveListener(null); 260 } 261 } 262 263 MusicUtils.unbindFromService(mToken); 264 try { 265 if ("nowplaying".equals(mPlaylist)) { 266 unregisterReceiverSafe(mNowPlayingListener); 267 } else { 268 unregisterReceiverSafe(mTrackListListener); 269 } 270 } catch (IllegalArgumentException ex) { 271 // we end up here in case we never registered the listeners 272 } 273 274 // If we have an adapter and didn't send it off to another activity yet, we should 275 // close its cursor, which we do by assigning a null cursor to it. Doing this 276 // instead of closing the cursor directly keeps the framework from accessing 277 // the closed cursor later. 278 if (!mAdapterSent && mAdapter != null) { 279 mAdapter.changeCursor(null); 280 } 281 // Because we pass the adapter to the next activity, we need to make 282 // sure it doesn't keep a reference to this activity. We can do this 283 // by clearing its DatasetObservers, which setListAdapter(null) does. 284 setListAdapter(null); 285 mAdapter = null; 286 unregisterReceiverSafe(mScanListener); 287 super.onDestroy(); 288 } 289 290 /** 291 * Unregister a receiver, but eat the exception that is thrown if the 292 * receiver was never registered to begin with. This is a little easier 293 * than keeping track of whether the receivers have actually been 294 * registered by the time onDestroy() is called. 295 */ 296 private void unregisterReceiverSafe(BroadcastReceiver receiver) { 297 try { 298 unregisterReceiver(receiver); 299 } catch (IllegalArgumentException e) { 300 // ignore 301 } 302 } 303 304 @Override 305 public void onResume() { 306 super.onResume(); 307 if (mTrackCursor != null) { 308 getListView().invalidateViews(); 309 } 310 MusicUtils.setSpinnerState(this); 311 } 312 @Override 313 public void onPause() { 314 mReScanHandler.removeCallbacksAndMessages(null); 315 super.onPause(); 316 } 317 318 /* 319 * This listener gets called when the media scanner starts up or finishes, and 320 * when the sd card is unmounted. 321 */ 322 private BroadcastReceiver mScanListener = new BroadcastReceiver() { 323 @Override 324 public void onReceive(Context context, Intent intent) { 325 String action = intent.getAction(); 326 if (Intent.ACTION_MEDIA_SCANNER_STARTED.equals(action) || 327 Intent.ACTION_MEDIA_SCANNER_FINISHED.equals(action)) { 328 MusicUtils.setSpinnerState(TrackBrowserActivity.this); 329 } 330 mReScanHandler.sendEmptyMessage(0); 331 } 332 }; 333 334 private Handler mReScanHandler = new Handler() { 335 @Override 336 public void handleMessage(Message msg) { 337 if (mAdapter != null) { 338 getTrackCursor(mAdapter.getQueryHandler(), null, true); 339 } 340 // if the query results in a null cursor, onQueryComplete() will 341 // call init(), which will post a delayed message to this handler 342 // in order to try again. 343 } 344 }; 345 346 public void onSaveInstanceState(Bundle outcicle) { 347 // need to store the selected item so we don't lose it in case 348 // of an orientation switch. Otherwise we could lose it while 349 // in the middle of specifying a playlist to add the item to. 350 outcicle.putLong("selectedtrack", mSelectedId); 351 outcicle.putString("artist", mArtistId); 352 outcicle.putString("album", mAlbumId); 353 outcicle.putString("playlist", mPlaylist); 354 outcicle.putString("genre", mGenre); 355 outcicle.putBoolean("editmode", mEditMode); 356 super.onSaveInstanceState(outcicle); 357 } 358 359 public void init(Cursor newCursor, boolean isLimited) { 360 361 if (mAdapter == null) { 362 return; 363 } 364 mAdapter.changeCursor(newCursor); // also sets mTrackCursor 365 366 if (mTrackCursor == null) { 367 MusicUtils.displayDatabaseError(this); 368 closeContextMenu(); 369 mReScanHandler.sendEmptyMessageDelayed(0, 1000); 370 return; 371 } 372 373 MusicUtils.hideDatabaseError(this); 374 mUseLastListPos = MusicUtils.updateButtonBar(this, R.id.songtab); 375 setTitle(); 376 377 // Restore previous position 378 if (mLastListPosCourse >= 0 && mUseLastListPos) { 379 ListView lv = getListView(); 380 // this hack is needed because otherwise the position doesn't change 381 // for the 2nd (non-limited) cursor 382 lv.setAdapter(lv.getAdapter()); 383 lv.setSelectionFromTop(mLastListPosCourse, mLastListPosFine); 384 if (!isLimited) { 385 mLastListPosCourse = -1; 386 } 387 } 388 389 // When showing the queue, position the selection on the currently playing track 390 // Otherwise, position the selection on the first matching artist, if any 391 IntentFilter f = new IntentFilter(); 392 f.addAction(MediaPlaybackService.META_CHANGED); 393 f.addAction(MediaPlaybackService.QUEUE_CHANGED); 394 if ("nowplaying".equals(mPlaylist)) { 395 try { 396 int cur = MusicUtils.sService.getQueuePosition(); 397 setSelection(cur); 398 registerReceiver(mNowPlayingListener, new IntentFilter(f)); 399 mNowPlayingListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED)); 400 } catch (RemoteException ex) { 401 } 402 } else { 403 String key = getIntent().getStringExtra("artist"); 404 if (key != null) { 405 int keyidx = mTrackCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID); 406 mTrackCursor.moveToFirst(); 407 while (! mTrackCursor.isAfterLast()) { 408 String artist = mTrackCursor.getString(keyidx); 409 if (artist.equals(key)) { 410 setSelection(mTrackCursor.getPosition()); 411 break; 412 } 413 mTrackCursor.moveToNext(); 414 } 415 } 416 registerReceiver(mTrackListListener, new IntentFilter(f)); 417 mTrackListListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED)); 418 } 419 } 420 421 private void setAlbumArtBackground() { 422 try { 423 long albumid = Long.valueOf(mAlbumId); 424 Bitmap bm = MusicUtils.getArtwork(TrackBrowserActivity.this, -1, albumid, false); 425 if (bm != null) { 426 MusicUtils.setBackground(mTrackList, bm); 427 mTrackList.setCacheColorHint(0); 428 return; 429 } 430 } catch (Exception ex) { 431 } 432 mTrackList.setBackgroundResource(0); 433 mTrackList.setCacheColorHint(0xff000000); 434 } 435 436 private void setTitle() { 437 438 CharSequence fancyName = null; 439 if (mAlbumId != null) { 440 int numresults = mTrackCursor != null ? mTrackCursor.getCount() : 0; 441 if (numresults > 0) { 442 mTrackCursor.moveToFirst(); 443 int idx = mTrackCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM); 444 fancyName = mTrackCursor.getString(idx); 445 // For compilation albums show only the album title, 446 // but for regular albums show "artist - album". 447 // To determine whether something is a compilation 448 // album, do a query for the artist + album of the 449 // first item, and see if it returns the same number 450 // of results as the album query. 451 String where = MediaStore.Audio.Media.ALBUM_ID + "='" + mAlbumId + 452 "' AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + 453 mTrackCursor.getLong(mTrackCursor.getColumnIndexOrThrow( 454 MediaStore.Audio.Media.ARTIST_ID)); 455 Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 456 new String[] {MediaStore.Audio.Media.ALBUM}, where, null, null); 457 if (cursor != null) { 458 if (cursor.getCount() != numresults) { 459 // compilation album 460 fancyName = mTrackCursor.getString(idx); 461 } 462 cursor.deactivate(); 463 } 464 if (fancyName == null || fancyName.equals(MediaStore.UNKNOWN_STRING)) { 465 fancyName = getString(R.string.unknown_album_name); 466 } 467 } 468 } else if (mPlaylist != null) { 469 if (mPlaylist.equals("nowplaying")) { 470 if (MusicUtils.getCurrentShuffleMode() == MediaPlaybackService.SHUFFLE_AUTO) { 471 fancyName = getText(R.string.partyshuffle_title); 472 } else { 473 fancyName = getText(R.string.nowplaying_title); 474 } 475 } else if (mPlaylist.equals("podcasts")){ 476 fancyName = getText(R.string.podcasts_title); 477 } else if (mPlaylist.equals("recentlyadded")){ 478 fancyName = getText(R.string.recentlyadded_title); 479 } else { 480 String [] cols = new String [] { 481 MediaStore.Audio.Playlists.NAME 482 }; 483 Cursor cursor = MusicUtils.query(this, 484 ContentUris.withAppendedId(Playlists.EXTERNAL_CONTENT_URI, Long.valueOf(mPlaylist)), 485 cols, null, null, null); 486 if (cursor != null) { 487 if (cursor.getCount() != 0) { 488 cursor.moveToFirst(); 489 fancyName = cursor.getString(0); 490 } 491 cursor.deactivate(); 492 } 493 } 494 } else if (mGenre != null) { 495 String [] cols = new String [] { 496 MediaStore.Audio.Genres.NAME 497 }; 498 Cursor cursor = MusicUtils.query(this, 499 ContentUris.withAppendedId(MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, Long.valueOf(mGenre)), 500 cols, null, null, null); 501 if (cursor != null) { 502 if (cursor.getCount() != 0) { 503 cursor.moveToFirst(); 504 fancyName = cursor.getString(0); 505 } 506 cursor.deactivate(); 507 } 508 } 509 510 if (fancyName != null) { 511 setTitle(fancyName); 512 } else { 513 setTitle(R.string.tracks_title); 514 } 515 } 516 517 private TouchInterceptor.DropListener mDropListener = 518 new TouchInterceptor.DropListener() { 519 public void drop(int from, int to) { 520 if (mTrackCursor instanceof NowPlayingCursor) { 521 // update the currently playing list 522 NowPlayingCursor c = (NowPlayingCursor) mTrackCursor; 523 c.moveItem(from, to); 524 ((TrackListAdapter)getListAdapter()).notifyDataSetChanged(); 525 getListView().invalidateViews(); 526 mDeletedOneRow = true; 527 } else { 528 // update a saved playlist 529 MediaStore.Audio.Playlists.Members.moveItem(getContentResolver(), 530 Long.valueOf(mPlaylist), from, to); 531 } 532 } 533 }; 534 535 private TouchInterceptor.RemoveListener mRemoveListener = 536 new TouchInterceptor.RemoveListener() { 537 public void remove(int which) { 538 removePlaylistItem(which); 539 } 540 }; 541 542 private void removePlaylistItem(int which) { 543 View v = mTrackList.getChildAt(which - mTrackList.getFirstVisiblePosition()); 544 if (v == null) { 545 Log.d(LOGTAG, "No view when removing playlist item " + which); 546 return; 547 } 548 try { 549 if (MusicUtils.sService != null 550 && which != MusicUtils.sService.getQueuePosition()) { 551 mDeletedOneRow = true; 552 } 553 } catch (RemoteException e) { 554 // Service died, so nothing playing. 555 mDeletedOneRow = true; 556 } 557 v.setVisibility(View.GONE); 558 mTrackList.invalidateViews(); 559 if (mTrackCursor instanceof NowPlayingCursor) { 560 ((NowPlayingCursor)mTrackCursor).removeItem(which); 561 } else { 562 int colidx = mTrackCursor.getColumnIndexOrThrow( 563 MediaStore.Audio.Playlists.Members._ID); 564 mTrackCursor.moveToPosition(which); 565 long id = mTrackCursor.getLong(colidx); 566 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 567 Long.valueOf(mPlaylist)); 568 getContentResolver().delete( 569 ContentUris.withAppendedId(uri, id), null, null); 570 } 571 v.setVisibility(View.VISIBLE); 572 mTrackList.invalidateViews(); 573 } 574 575 private BroadcastReceiver mTrackListListener = new BroadcastReceiver() { 576 @Override 577 public void onReceive(Context context, Intent intent) { 578 getListView().invalidateViews(); 579 if (!mEditMode) { 580 MusicUtils.updateNowPlaying(TrackBrowserActivity.this); 581 } 582 } 583 }; 584 585 private BroadcastReceiver mNowPlayingListener = new BroadcastReceiver() { 586 @Override 587 public void onReceive(Context context, Intent intent) { 588 if (intent.getAction().equals(MediaPlaybackService.META_CHANGED)) { 589 getListView().invalidateViews(); 590 } else if (intent.getAction().equals(MediaPlaybackService.QUEUE_CHANGED)) { 591 if (mDeletedOneRow) { 592 // This is the notification for a single row that was 593 // deleted previously, which is already reflected in 594 // the UI. 595 mDeletedOneRow = false; 596 return; 597 } 598 // The service could disappear while the broadcast was in flight, 599 // so check to see if it's still valid 600 if (MusicUtils.sService == null) { 601 finish(); 602 return; 603 } 604 if (mAdapter != null) { 605 Cursor c = new NowPlayingCursor(MusicUtils.sService, mCursorCols); 606 if (c.getCount() == 0) { 607 finish(); 608 return; 609 } 610 mAdapter.changeCursor(c); 611 } 612 } 613 } 614 }; 615 616 // Cursor should be positioned on the entry to be checked 617 // Returns false if the entry matches the naming pattern used for recordings, 618 // or if it is marked as not music in the database. 619 private boolean isMusic(Cursor c) { 620 int titleidx = c.getColumnIndex(MediaStore.Audio.Media.TITLE); 621 int albumidx = c.getColumnIndex(MediaStore.Audio.Media.ALBUM); 622 int artistidx = c.getColumnIndex(MediaStore.Audio.Media.ARTIST); 623 624 String title = c.getString(titleidx); 625 String album = c.getString(albumidx); 626 String artist = c.getString(artistidx); 627 if (MediaStore.UNKNOWN_STRING.equals(album) && 628 MediaStore.UNKNOWN_STRING.equals(artist) && 629 title != null && 630 title.startsWith("recording")) { 631 // not music 632 return false; 633 } 634 635 int ismusic_idx = c.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC); 636 boolean ismusic = true; 637 if (ismusic_idx >= 0) { 638 ismusic = mTrackCursor.getInt(ismusic_idx) != 0; 639 } 640 return ismusic; 641 } 642 643 @Override 644 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 645 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection); 646 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist); 647 MusicUtils.makePlaylistMenu(this, sub); 648 if (mEditMode) { 649 menu.add(0, REMOVE, 0, R.string.remove_from_playlist); 650 } 651 menu.add(0, USE_AS_RINGTONE, 0, R.string.ringtone_menu); 652 menu.add(0, DELETE_ITEM, 0, R.string.delete_item); 653 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn; 654 mSelectedPosition = mi.position; 655 mTrackCursor.moveToPosition(mSelectedPosition); 656 try { 657 int id_idx = mTrackCursor.getColumnIndexOrThrow( 658 MediaStore.Audio.Playlists.Members.AUDIO_ID); 659 mSelectedId = mTrackCursor.getLong(id_idx); 660 } catch (IllegalArgumentException ex) { 661 mSelectedId = mi.id; 662 } 663 // only add the 'search' menu if the selected item is music 664 if (isMusic(mTrackCursor)) { 665 menu.add(0, SEARCH, 0, R.string.search_title); 666 } 667 mCurrentAlbumName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 668 MediaStore.Audio.Media.ALBUM)); 669 mCurrentArtistNameForAlbum = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 670 MediaStore.Audio.Media.ARTIST)); 671 mCurrentTrackName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 672 MediaStore.Audio.Media.TITLE)); 673 menu.setHeaderTitle(mCurrentTrackName); 674 } 675 676 @Override 677 public boolean onContextItemSelected(MenuItem item) { 678 switch (item.getItemId()) { 679 case PLAY_SELECTION: { 680 // play the track 681 int position = mSelectedPosition; 682 MusicUtils.playAll(this, mTrackCursor, position); 683 return true; 684 } 685 686 case QUEUE: { 687 long [] list = new long[] { mSelectedId }; 688 MusicUtils.addToCurrentPlaylist(this, list); 689 return true; 690 } 691 692 case NEW_PLAYLIST: { 693 Intent intent = new Intent(); 694 intent.setClass(this, CreatePlaylist.class); 695 startActivityForResult(intent, NEW_PLAYLIST); 696 return true; 697 } 698 699 case PLAYLIST_SELECTED: { 700 long [] list = new long[] { mSelectedId }; 701 long playlist = item.getIntent().getLongExtra("playlist", 0); 702 MusicUtils.addToPlaylist(this, list, playlist); 703 return true; 704 } 705 706 case USE_AS_RINGTONE: 707 // Set the system setting to make this the current ringtone 708 MusicUtils.setRingtone(this, mSelectedId); 709 return true; 710 711 case DELETE_ITEM: { 712 long [] list = new long[1]; 713 list[0] = (int) mSelectedId; 714 Bundle b = new Bundle(); 715 String f = getString(R.string.delete_song_desc); 716 String desc = String.format(f, mCurrentTrackName); 717 b.putString("description", desc); 718 b.putLongArray("items", list); 719 Intent intent = new Intent(); 720 intent.setClass(this, DeleteItems.class); 721 intent.putExtras(b); 722 startActivityForResult(intent, -1); 723 return true; 724 } 725 726 case REMOVE: 727 removePlaylistItem(mSelectedPosition); 728 return true; 729 730 case SEARCH: 731 doSearch(); 732 return true; 733 } 734 return super.onContextItemSelected(item); 735 } 736 737 void doSearch() { 738 CharSequence title = null; 739 String query = null; 740 741 Intent i = new Intent(); 742 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH); 743 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 744 745 title = mCurrentTrackName; 746 if (MediaStore.UNKNOWN_STRING.equals(mCurrentArtistNameForAlbum)) { 747 query = mCurrentTrackName; 748 } else { 749 query = mCurrentArtistNameForAlbum + " " + mCurrentTrackName; 750 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum); 751 } 752 if (MediaStore.UNKNOWN_STRING.equals(mCurrentAlbumName)) { 753 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName); 754 } 755 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, "audio/*"); 756 title = getString(R.string.mediasearch, title); 757 i.putExtra(SearchManager.QUERY, query); 758 759 startActivity(Intent.createChooser(i, title)); 760 } 761 762 // In order to use alt-up/down as a shortcut for moving the selected item 763 // in the list, we need to override dispatchKeyEvent, not onKeyDown. 764 // (onKeyDown never sees these events, since they are handled by the list) 765 @Override 766 public boolean dispatchKeyEvent(KeyEvent event) { 767 if (mPlaylist != null && event.getMetaState() != 0 && 768 event.getAction() == KeyEvent.ACTION_DOWN) { 769 switch (event.getKeyCode()) { 770 case KeyEvent.KEYCODE_DPAD_UP: 771 moveItem(true); 772 return true; 773 case KeyEvent.KEYCODE_DPAD_DOWN: 774 moveItem(false); 775 return true; 776 case KeyEvent.KEYCODE_DEL: 777 removeItem(); 778 return true; 779 } 780 } 781 782 return super.dispatchKeyEvent(event); 783 } 784 785 private void removeItem() { 786 int curcount = mTrackCursor.getCount(); 787 int curpos = mTrackList.getSelectedItemPosition(); 788 if (curcount == 0 || curpos < 0) { 789 return; 790 } 791 792 if ("nowplaying".equals(mPlaylist)) { 793 // remove track from queue 794 795 // Work around bug 902971. To get quick visual feedback 796 // of the deletion of the item, hide the selected view. 797 try { 798 if (curpos != MusicUtils.sService.getQueuePosition()) { 799 mDeletedOneRow = true; 800 } 801 } catch (RemoteException ex) { 802 } 803 View v = mTrackList.getSelectedView(); 804 v.setVisibility(View.GONE); 805 mTrackList.invalidateViews(); 806 ((NowPlayingCursor)mTrackCursor).removeItem(curpos); 807 v.setVisibility(View.VISIBLE); 808 mTrackList.invalidateViews(); 809 } else { 810 // remove track from playlist 811 int colidx = mTrackCursor.getColumnIndexOrThrow( 812 MediaStore.Audio.Playlists.Members._ID); 813 mTrackCursor.moveToPosition(curpos); 814 long id = mTrackCursor.getLong(colidx); 815 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 816 Long.valueOf(mPlaylist)); 817 getContentResolver().delete( 818 ContentUris.withAppendedId(uri, id), null, null); 819 curcount--; 820 if (curcount == 0) { 821 finish(); 822 } else { 823 mTrackList.setSelection(curpos < curcount ? curpos : curcount); 824 } 825 } 826 } 827 828 private void moveItem(boolean up) { 829 int curcount = mTrackCursor.getCount(); 830 int curpos = mTrackList.getSelectedItemPosition(); 831 if ( (up && curpos < 1) || (!up && curpos >= curcount - 1)) { 832 return; 833 } 834 835 if (mTrackCursor instanceof NowPlayingCursor) { 836 NowPlayingCursor c = (NowPlayingCursor) mTrackCursor; 837 c.moveItem(curpos, up ? curpos - 1 : curpos + 1); 838 ((TrackListAdapter)getListAdapter()).notifyDataSetChanged(); 839 getListView().invalidateViews(); 840 mDeletedOneRow = true; 841 if (up) { 842 mTrackList.setSelection(curpos - 1); 843 } else { 844 mTrackList.setSelection(curpos + 1); 845 } 846 } else { 847 int colidx = mTrackCursor.getColumnIndexOrThrow( 848 MediaStore.Audio.Playlists.Members.PLAY_ORDER); 849 mTrackCursor.moveToPosition(curpos); 850 int currentplayidx = mTrackCursor.getInt(colidx); 851 Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external", 852 Long.valueOf(mPlaylist)); 853 ContentValues values = new ContentValues(); 854 String where = MediaStore.Audio.Playlists.Members._ID + "=?"; 855 String [] wherearg = new String[1]; 856 ContentResolver res = getContentResolver(); 857 if (up) { 858 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx - 1); 859 wherearg[0] = mTrackCursor.getString(0); 860 res.update(baseUri, values, where, wherearg); 861 mTrackCursor.moveToPrevious(); 862 } else { 863 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx + 1); 864 wherearg[0] = mTrackCursor.getString(0); 865 res.update(baseUri, values, where, wherearg); 866 mTrackCursor.moveToNext(); 867 } 868 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx); 869 wherearg[0] = mTrackCursor.getString(0); 870 res.update(baseUri, values, where, wherearg); 871 } 872 } 873 874 @Override 875 protected void onListItemClick(ListView l, View v, int position, long id) 876 { 877 if (mTrackCursor.getCount() == 0) { 878 return; 879 } 880 // When selecting a track from the queue, just jump there instead of 881 // reloading the queue. This is both faster, and prevents accidentally 882 // dropping out of party shuffle. 883 if (mTrackCursor instanceof NowPlayingCursor) { 884 if (MusicUtils.sService != null) { 885 try { 886 MusicUtils.sService.setQueuePosition(position); 887 return; 888 } catch (RemoteException ex) { 889 } 890 } 891 } 892 MusicUtils.playAll(this, mTrackCursor, position); 893 } 894 895 @Override 896 public boolean onCreateOptionsMenu(Menu menu) { 897 /* This activity is used for a number of different browsing modes, and the menu can 898 * be different for each of them: 899 * - all tracks, optionally restricted to an album, artist or playlist 900 * - the list of currently playing songs 901 */ 902 super.onCreateOptionsMenu(menu); 903 if (mPlaylist == null) { 904 menu.add(0, PLAY_ALL, 0, R.string.play_all).setIcon(R.drawable.ic_menu_play_clip); 905 } 906 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 907 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle); 908 if (mPlaylist != null) { 909 menu.add(0, SAVE_AS_PLAYLIST, 0, R.string.save_as_playlist).setIcon(android.R.drawable.ic_menu_save); 910 if (mPlaylist.equals("nowplaying")) { 911 menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(R.drawable.ic_menu_clear_playlist); 912 } 913 } 914 return true; 915 } 916 917 @Override 918 public boolean onPrepareOptionsMenu(Menu menu) { 919 MusicUtils.setPartyShuffleMenuIcon(menu); 920 return super.onPrepareOptionsMenu(menu); 921 } 922 923 @Override 924 public boolean onOptionsItemSelected(MenuItem item) { 925 Intent intent; 926 Cursor cursor; 927 switch (item.getItemId()) { 928 case PLAY_ALL: { 929 MusicUtils.playAll(this, mTrackCursor); 930 return true; 931 } 932 933 case PARTY_SHUFFLE: 934 MusicUtils.togglePartyShuffle(); 935 break; 936 937 case SHUFFLE_ALL: 938 // Should 'shuffle all' shuffle ALL, or only the tracks shown? 939 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 940 new String [] { MediaStore.Audio.Media._ID}, 941 MediaStore.Audio.Media.IS_MUSIC + "=1", null, 942 MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 943 if (cursor != null) { 944 MusicUtils.shuffleAll(this, cursor); 945 cursor.close(); 946 } 947 return true; 948 949 case SAVE_AS_PLAYLIST: 950 intent = new Intent(); 951 intent.setClass(this, CreatePlaylist.class); 952 startActivityForResult(intent, SAVE_AS_PLAYLIST); 953 return true; 954 955 case CLEAR_PLAYLIST: 956 // We only clear the current playlist 957 MusicUtils.clearQueue(); 958 return true; 959 } 960 return super.onOptionsItemSelected(item); 961 } 962 963 @Override 964 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 965 switch (requestCode) { 966 case SCAN_DONE: 967 if (resultCode == RESULT_CANCELED) { 968 finish(); 969 } else { 970 getTrackCursor(mAdapter.getQueryHandler(), null, true); 971 } 972 break; 973 974 case NEW_PLAYLIST: 975 if (resultCode == RESULT_OK) { 976 Uri uri = intent.getData(); 977 if (uri != null) { 978 long [] list = new long[] { mSelectedId }; 979 MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment())); 980 } 981 } 982 break; 983 984 case SAVE_AS_PLAYLIST: 985 if (resultCode == RESULT_OK) { 986 Uri uri = intent.getData(); 987 if (uri != null) { 988 long [] list = MusicUtils.getSongListForCursor(mTrackCursor); 989 int plid = Integer.parseInt(uri.getLastPathSegment()); 990 MusicUtils.addToPlaylist(this, list, plid); 991 } 992 } 993 break; 994 } 995 } 996 997 private Cursor getTrackCursor(TrackListAdapter.TrackQueryHandler queryhandler, String filter, 998 boolean async) { 999 1000 if (queryhandler == null) { 1001 throw new IllegalArgumentException(); 1002 } 1003 1004 Cursor ret = null; 1005 mSortOrder = MediaStore.Audio.Media.TITLE_KEY; 1006 StringBuilder where = new StringBuilder(); 1007 where.append(MediaStore.Audio.Media.TITLE + " != ''"); 1008 1009 if (mGenre != null) { 1010 Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", 1011 Integer.valueOf(mGenre)); 1012 if (!TextUtils.isEmpty(filter)) { 1013 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1014 } 1015 mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER; 1016 ret = queryhandler.doQuery(uri, 1017 mCursorCols, where.toString(), null, mSortOrder, async); 1018 } else if (mPlaylist != null) { 1019 if (mPlaylist.equals("nowplaying")) { 1020 if (MusicUtils.sService != null) { 1021 ret = new NowPlayingCursor(MusicUtils.sService, mCursorCols); 1022 if (ret.getCount() == 0) { 1023 finish(); 1024 } 1025 } else { 1026 // Nothing is playing. 1027 } 1028 } else if (mPlaylist.equals("podcasts")) { 1029 where.append(" AND " + MediaStore.Audio.Media.IS_PODCAST + "=1"); 1030 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1031 if (!TextUtils.isEmpty(filter)) { 1032 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1033 } 1034 ret = queryhandler.doQuery(uri, 1035 mCursorCols, where.toString(), null, 1036 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1037 } else if (mPlaylist.equals("recentlyadded")) { 1038 // do a query for all songs added in the last X weeks 1039 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1040 if (!TextUtils.isEmpty(filter)) { 1041 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1042 } 1043 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7); 1044 where.append(" AND " + MediaStore.MediaColumns.DATE_ADDED + ">"); 1045 where.append(System.currentTimeMillis() / 1000 - X); 1046 ret = queryhandler.doQuery(uri, 1047 mCursorCols, where.toString(), null, 1048 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1049 } else { 1050 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 1051 Long.valueOf(mPlaylist)); 1052 if (!TextUtils.isEmpty(filter)) { 1053 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1054 } 1055 mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER; 1056 ret = queryhandler.doQuery(uri, mPlaylistMemberCols, 1057 where.toString(), null, mSortOrder, async); 1058 } 1059 } else { 1060 if (mAlbumId != null) { 1061 where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "=" + mAlbumId); 1062 mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder; 1063 } 1064 if (mArtistId != null) { 1065 where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId); 1066 } 1067 where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1"); 1068 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1069 if (!TextUtils.isEmpty(filter)) { 1070 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1071 } 1072 ret = queryhandler.doQuery(uri, 1073 mCursorCols, where.toString() , null, mSortOrder, async); 1074 } 1075 1076 // This special case is for the "nowplaying" cursor, which cannot be handled 1077 // asynchronously using AsyncQueryHandler, so we do some extra initialization here. 1078 if (ret != null && async) { 1079 init(ret, false); 1080 setTitle(); 1081 } 1082 return ret; 1083 } 1084 1085 private class NowPlayingCursor extends AbstractCursor 1086 { 1087 public NowPlayingCursor(IMediaPlaybackService service, String [] cols) 1088 { 1089 mCols = cols; 1090 mService = service; 1091 makeNowPlayingCursor(); 1092 } 1093 private void makeNowPlayingCursor() { 1094 mCurrentPlaylistCursor = null; 1095 try { 1096 mNowPlaying = mService.getQueue(); 1097 } catch (RemoteException ex) { 1098 mNowPlaying = new long[0]; 1099 } 1100 mSize = mNowPlaying.length; 1101 if (mSize == 0) { 1102 return; 1103 } 1104 1105 StringBuilder where = new StringBuilder(); 1106 where.append(MediaStore.Audio.Media._ID + " IN ("); 1107 for (int i = 0; i < mSize; i++) { 1108 where.append(mNowPlaying[i]); 1109 if (i < mSize - 1) { 1110 where.append(","); 1111 } 1112 } 1113 where.append(")"); 1114 1115 mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this, 1116 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1117 mCols, where.toString(), null, MediaStore.Audio.Media._ID); 1118 1119 if (mCurrentPlaylistCursor == null) { 1120 mSize = 0; 1121 return; 1122 } 1123 1124 int size = mCurrentPlaylistCursor.getCount(); 1125 mCursorIdxs = new long[size]; 1126 mCurrentPlaylistCursor.moveToFirst(); 1127 int colidx = mCurrentPlaylistCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1128 for (int i = 0; i < size; i++) { 1129 mCursorIdxs[i] = mCurrentPlaylistCursor.getLong(colidx); 1130 mCurrentPlaylistCursor.moveToNext(); 1131 } 1132 mCurrentPlaylistCursor.moveToFirst(); 1133 mCurPos = -1; 1134 1135 // At this point we can verify the 'now playing' list we got 1136 // earlier to make sure that all the items in there still exist 1137 // in the database, and remove those that aren't. This way we 1138 // don't get any blank items in the list. 1139 try { 1140 int removed = 0; 1141 for (int i = mNowPlaying.length - 1; i >= 0; i--) { 1142 long trackid = mNowPlaying[i]; 1143 int crsridx = Arrays.binarySearch(mCursorIdxs, trackid); 1144 if (crsridx < 0) { 1145 //Log.i("@@@@@", "item no longer exists in db: " + trackid); 1146 removed += mService.removeTrack(trackid); 1147 } 1148 } 1149 if (removed > 0) { 1150 mNowPlaying = mService.getQueue(); 1151 mSize = mNowPlaying.length; 1152 if (mSize == 0) { 1153 mCursorIdxs = null; 1154 return; 1155 } 1156 } 1157 } catch (RemoteException ex) { 1158 mNowPlaying = new long[0]; 1159 } 1160 } 1161 1162 @Override 1163 public int getCount() 1164 { 1165 return mSize; 1166 } 1167 1168 @Override 1169 public boolean onMove(int oldPosition, int newPosition) 1170 { 1171 if (oldPosition == newPosition) 1172 return true; 1173 1174 if (mNowPlaying == null || mCursorIdxs == null || newPosition >= mNowPlaying.length) { 1175 return false; 1176 } 1177 1178 // The cursor doesn't have any duplicates in it, and is not ordered 1179 // in queue-order, so we need to figure out where in the cursor we 1180 // should be. 1181 1182 long newid = mNowPlaying[newPosition]; 1183 int crsridx = Arrays.binarySearch(mCursorIdxs, newid); 1184 mCurrentPlaylistCursor.moveToPosition(crsridx); 1185 mCurPos = newPosition; 1186 1187 return true; 1188 } 1189 1190 public boolean removeItem(int which) 1191 { 1192 try { 1193 if (mService.removeTracks(which, which) == 0) { 1194 return false; // delete failed 1195 } 1196 int i = (int) which; 1197 mSize--; 1198 while (i < mSize) { 1199 mNowPlaying[i] = mNowPlaying[i+1]; 1200 i++; 1201 } 1202 onMove(-1, (int) mCurPos); 1203 } catch (RemoteException ex) { 1204 } 1205 return true; 1206 } 1207 1208 public void moveItem(int from, int to) { 1209 try { 1210 mService.moveQueueItem(from, to); 1211 mNowPlaying = mService.getQueue(); 1212 onMove(-1, mCurPos); // update the underlying cursor 1213 } catch (RemoteException ex) { 1214 } 1215 } 1216 1217 private void dump() { 1218 String where = "("; 1219 for (int i = 0; i < mSize; i++) { 1220 where += mNowPlaying[i]; 1221 if (i < mSize - 1) { 1222 where += ","; 1223 } 1224 } 1225 where += ")"; 1226 Log.i("NowPlayingCursor: ", where); 1227 } 1228 1229 @Override 1230 public String getString(int column) 1231 { 1232 try { 1233 return mCurrentPlaylistCursor.getString(column); 1234 } catch (Exception ex) { 1235 onChange(true); 1236 return ""; 1237 } 1238 } 1239 1240 @Override 1241 public short getShort(int column) 1242 { 1243 return mCurrentPlaylistCursor.getShort(column); 1244 } 1245 1246 @Override 1247 public int getInt(int column) 1248 { 1249 try { 1250 return mCurrentPlaylistCursor.getInt(column); 1251 } catch (Exception ex) { 1252 onChange(true); 1253 return 0; 1254 } 1255 } 1256 1257 @Override 1258 public long getLong(int column) 1259 { 1260 try { 1261 return mCurrentPlaylistCursor.getLong(column); 1262 } catch (Exception ex) { 1263 onChange(true); 1264 return 0; 1265 } 1266 } 1267 1268 @Override 1269 public float getFloat(int column) 1270 { 1271 return mCurrentPlaylistCursor.getFloat(column); 1272 } 1273 1274 @Override 1275 public double getDouble(int column) 1276 { 1277 return mCurrentPlaylistCursor.getDouble(column); 1278 } 1279 1280 @Override 1281 public boolean isNull(int column) 1282 { 1283 return mCurrentPlaylistCursor.isNull(column); 1284 } 1285 1286 @Override 1287 public String[] getColumnNames() 1288 { 1289 return mCols; 1290 } 1291 1292 @Override 1293 public void deactivate() 1294 { 1295 if (mCurrentPlaylistCursor != null) 1296 mCurrentPlaylistCursor.deactivate(); 1297 } 1298 1299 @Override 1300 public boolean requery() 1301 { 1302 makeNowPlayingCursor(); 1303 return true; 1304 } 1305 1306 private String [] mCols; 1307 private Cursor mCurrentPlaylistCursor; // updated in onMove 1308 private int mSize; // size of the queue 1309 private long[] mNowPlaying; 1310 private long[] mCursorIdxs; 1311 private int mCurPos; 1312 private IMediaPlaybackService mService; 1313 } 1314 1315 static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer { 1316 boolean mIsNowPlaying; 1317 boolean mDisableNowPlayingIndicator; 1318 1319 int mTitleIdx; 1320 int mArtistIdx; 1321 int mDurationIdx; 1322 int mAudioIdIdx; 1323 1324 private final StringBuilder mBuilder = new StringBuilder(); 1325 private final String mUnknownArtist; 1326 private final String mUnknownAlbum; 1327 1328 private AlphabetIndexer mIndexer; 1329 1330 private TrackBrowserActivity mActivity = null; 1331 private TrackQueryHandler mQueryHandler; 1332 private String mConstraint = null; 1333 private boolean mConstraintIsValid = false; 1334 1335 static class ViewHolder { 1336 TextView line1; 1337 TextView line2; 1338 TextView duration; 1339 ImageView play_indicator; 1340 CharArrayBuffer buffer1; 1341 char [] buffer2; 1342 } 1343 1344 class TrackQueryHandler extends AsyncQueryHandler { 1345 1346 class QueryArgs { 1347 public Uri uri; 1348 public String [] projection; 1349 public String selection; 1350 public String [] selectionArgs; 1351 public String orderBy; 1352 } 1353 1354 TrackQueryHandler(ContentResolver res) { 1355 super(res); 1356 } 1357 1358 public Cursor doQuery(Uri uri, String[] projection, 1359 String selection, String[] selectionArgs, 1360 String orderBy, boolean async) { 1361 if (async) { 1362 // Get 100 results first, which is enough to allow the user to start scrolling, 1363 // while still being very fast. 1364 Uri limituri = uri.buildUpon().appendQueryParameter("limit", "100").build(); 1365 QueryArgs args = new QueryArgs(); 1366 args.uri = uri; 1367 args.projection = projection; 1368 args.selection = selection; 1369 args.selectionArgs = selectionArgs; 1370 args.orderBy = orderBy; 1371 1372 startQuery(0, args, limituri, projection, selection, selectionArgs, orderBy); 1373 return null; 1374 } 1375 return MusicUtils.query(mActivity, 1376 uri, projection, selection, selectionArgs, orderBy); 1377 } 1378 1379 @Override 1380 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 1381 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity); 1382 mActivity.init(cursor, cookie != null); 1383 if (token == 0 && cookie != null && cursor != null && cursor.getCount() >= 100) { 1384 QueryArgs args = (QueryArgs) cookie; 1385 startQuery(1, null, args.uri, args.projection, args.selection, 1386 args.selectionArgs, args.orderBy); 1387 } 1388 } 1389 } 1390 1391 TrackListAdapter(Context context, TrackBrowserActivity currentactivity, 1392 int layout, Cursor cursor, String[] from, int[] to, 1393 boolean isnowplaying, boolean disablenowplayingindicator) { 1394 super(context, layout, cursor, from, to); 1395 mActivity = currentactivity; 1396 getColumnIndices(cursor); 1397 mIsNowPlaying = isnowplaying; 1398 mDisableNowPlayingIndicator = disablenowplayingindicator; 1399 mUnknownArtist = context.getString(R.string.unknown_artist_name); 1400 mUnknownAlbum = context.getString(R.string.unknown_album_name); 1401 1402 mQueryHandler = new TrackQueryHandler(context.getContentResolver()); 1403 } 1404 1405 public void setActivity(TrackBrowserActivity newactivity) { 1406 mActivity = newactivity; 1407 } 1408 1409 public TrackQueryHandler getQueryHandler() { 1410 return mQueryHandler; 1411 } 1412 1413 private void getColumnIndices(Cursor cursor) { 1414 if (cursor != null) { 1415 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); 1416 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); 1417 mDurationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); 1418 try { 1419 mAudioIdIdx = cursor.getColumnIndexOrThrow( 1420 MediaStore.Audio.Playlists.Members.AUDIO_ID); 1421 } catch (IllegalArgumentException ex) { 1422 mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1423 } 1424 1425 if (mIndexer != null) { 1426 mIndexer.setCursor(cursor); 1427 } else if (!mActivity.mEditMode) { 1428 String alpha = mActivity.getString(R.string.fast_scroll_alphabet); 1429 1430 mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha); 1431 } 1432 } 1433 } 1434 1435 @Override 1436 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1437 View v = super.newView(context, cursor, parent); 1438 ImageView iv = (ImageView) v.findViewById(R.id.icon); 1439 if (mActivity.mEditMode) { 1440 iv.setVisibility(View.VISIBLE); 1441 iv.setImageResource(R.drawable.ic_mp_move); 1442 } else { 1443 iv.setVisibility(View.GONE); 1444 } 1445 1446 ViewHolder vh = new ViewHolder(); 1447 vh.line1 = (TextView) v.findViewById(R.id.line1); 1448 vh.line2 = (TextView) v.findViewById(R.id.line2); 1449 vh.duration = (TextView) v.findViewById(R.id.duration); 1450 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator); 1451 vh.buffer1 = new CharArrayBuffer(100); 1452 vh.buffer2 = new char[200]; 1453 v.setTag(vh); 1454 return v; 1455 } 1456 1457 @Override 1458 public void bindView(View view, Context context, Cursor cursor) { 1459 1460 ViewHolder vh = (ViewHolder) view.getTag(); 1461 1462 cursor.copyStringToBuffer(mTitleIdx, vh.buffer1); 1463 vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied); 1464 1465 int secs = cursor.getInt(mDurationIdx) / 1000; 1466 if (secs == 0) { 1467 vh.duration.setText(""); 1468 } else { 1469 vh.duration.setText(MusicUtils.makeTimeString(context, secs)); 1470 } 1471 1472 final StringBuilder builder = mBuilder; 1473 builder.delete(0, builder.length()); 1474 1475 String name = cursor.getString(mArtistIdx); 1476 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 1477 builder.append(mUnknownArtist); 1478 } else { 1479 builder.append(name); 1480 } 1481 int len = builder.length(); 1482 if (vh.buffer2.length < len) { 1483 vh.buffer2 = new char[len]; 1484 } 1485 builder.getChars(0, len, vh.buffer2, 0); 1486 vh.line2.setText(vh.buffer2, 0, len); 1487 1488 ImageView iv = vh.play_indicator; 1489 long id = -1; 1490 if (MusicUtils.sService != null) { 1491 // TODO: IPC call on each bind?? 1492 try { 1493 if (mIsNowPlaying) { 1494 id = MusicUtils.sService.getQueuePosition(); 1495 } else { 1496 id = MusicUtils.sService.getAudioId(); 1497 } 1498 } catch (RemoteException ex) { 1499 } 1500 } 1501 1502 // Determining whether and where to show the "now playing indicator 1503 // is tricky, because we don't actually keep track of where the songs 1504 // in the current playlist came from after they've started playing. 1505 // 1506 // If the "current playlists" is shown, then we can simply match by position, 1507 // otherwise, we need to match by id. Match-by-id gets a little weird if 1508 // a song appears in a playlist more than once, and you're in edit-playlist 1509 // mode. In that case, both items will have the "now playing" indicator. 1510 // For this reason, we don't show the play indicator at all when in edit 1511 // playlist mode (except when you're viewing the "current playlist", 1512 // which is not really a playlist) 1513 if ( (mIsNowPlaying && cursor.getPosition() == id) || 1514 (!mIsNowPlaying && !mDisableNowPlayingIndicator && cursor.getLong(mAudioIdIdx) == id)) { 1515 iv.setImageResource(R.drawable.indicator_ic_mp_playing_list); 1516 iv.setVisibility(View.VISIBLE); 1517 } else { 1518 iv.setVisibility(View.GONE); 1519 } 1520 } 1521 1522 @Override 1523 public void changeCursor(Cursor cursor) { 1524 if (mActivity.isFinishing() && cursor != null) { 1525 cursor.close(); 1526 cursor = null; 1527 } 1528 if (cursor != mActivity.mTrackCursor) { 1529 mActivity.mTrackCursor = cursor; 1530 super.changeCursor(cursor); 1531 getColumnIndices(cursor); 1532 } 1533 } 1534 1535 @Override 1536 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 1537 String s = constraint.toString(); 1538 if (mConstraintIsValid && ( 1539 (s == null && mConstraint == null) || 1540 (s != null && s.equals(mConstraint)))) { 1541 return getCursor(); 1542 } 1543 Cursor c = mActivity.getTrackCursor(mQueryHandler, s, false); 1544 mConstraint = s; 1545 mConstraintIsValid = true; 1546 return c; 1547 } 1548 1549 // SectionIndexer methods 1550 1551 public Object[] getSections() { 1552 if (mIndexer != null) { 1553 return mIndexer.getSections(); 1554 } else { 1555 return null; 1556 } 1557 } 1558 1559 public int getPositionForSection(int section) { 1560 int pos = mIndexer.getPositionForSection(section); 1561 return pos; 1562 } 1563 1564 public int getSectionForPosition(int position) { 1565 return 0; 1566 } 1567 } 1568} 1569 1570