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