TrackBrowserActivity.java revision f33a575c2b09095d58ad7af527113f767e4c37b1
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 Cursor c = new NowPlayingCursor(MusicUtils.sService, mCursorCols); 590 if (c.getCount() == 0) { 591 finish(); 592 return; 593 } 594 mAdapter.changeCursor(c); 595 } 596 } 597 }; 598 599 // Cursor should be positioned on the entry to be checked 600 // Returns false if the entry matches the naming pattern used for recordings, 601 // or if it is marked as not music in the database. 602 private boolean isMusic(Cursor c) { 603 int titleidx = c.getColumnIndex(MediaStore.Audio.Media.TITLE); 604 int albumidx = c.getColumnIndex(MediaStore.Audio.Media.ALBUM); 605 int artistidx = c.getColumnIndex(MediaStore.Audio.Media.ARTIST); 606 607 String title = c.getString(titleidx); 608 String album = c.getString(albumidx); 609 String artist = c.getString(artistidx); 610 if (MediaStore.UNKNOWN_STRING.equals(album) && 611 MediaStore.UNKNOWN_STRING.equals(artist) && 612 title != null && 613 title.startsWith("recording")) { 614 // not music 615 return false; 616 } 617 618 int ismusic_idx = c.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC); 619 boolean ismusic = true; 620 if (ismusic_idx >= 0) { 621 ismusic = mTrackCursor.getInt(ismusic_idx) != 0; 622 } 623 return ismusic; 624 } 625 626 @Override 627 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 628 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection); 629 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist); 630 MusicUtils.makePlaylistMenu(this, sub); 631 if (mEditMode) { 632 menu.add(0, REMOVE, 0, R.string.remove_from_playlist); 633 } 634 menu.add(0, USE_AS_RINGTONE, 0, R.string.ringtone_menu); 635 menu.add(0, DELETE_ITEM, 0, R.string.delete_item); 636 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn; 637 mSelectedPosition = mi.position; 638 mTrackCursor.moveToPosition(mSelectedPosition); 639 try { 640 int id_idx = mTrackCursor.getColumnIndexOrThrow( 641 MediaStore.Audio.Playlists.Members.AUDIO_ID); 642 mSelectedId = mTrackCursor.getLong(id_idx); 643 } catch (IllegalArgumentException ex) { 644 mSelectedId = mi.id; 645 } 646 // only add the 'search' menu if the selected item is music 647 if (isMusic(mTrackCursor)) { 648 menu.add(0, SEARCH, 0, R.string.search_title); 649 } 650 mCurrentAlbumName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 651 MediaStore.Audio.Media.ALBUM)); 652 mCurrentArtistNameForAlbum = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 653 MediaStore.Audio.Media.ARTIST)); 654 mCurrentTrackName = mTrackCursor.getString(mTrackCursor.getColumnIndexOrThrow( 655 MediaStore.Audio.Media.TITLE)); 656 menu.setHeaderTitle(mCurrentTrackName); 657 } 658 659 @Override 660 public boolean onContextItemSelected(MenuItem item) { 661 switch (item.getItemId()) { 662 case PLAY_SELECTION: { 663 // play the track 664 int position = mSelectedPosition; 665 MusicUtils.playAll(this, mTrackCursor, position); 666 return true; 667 } 668 669 case QUEUE: { 670 long [] list = new long[] { mSelectedId }; 671 MusicUtils.addToCurrentPlaylist(this, list); 672 return true; 673 } 674 675 case NEW_PLAYLIST: { 676 Intent intent = new Intent(); 677 intent.setClass(this, CreatePlaylist.class); 678 startActivityForResult(intent, NEW_PLAYLIST); 679 return true; 680 } 681 682 case PLAYLIST_SELECTED: { 683 long [] list = new long[] { mSelectedId }; 684 long playlist = item.getIntent().getLongExtra("playlist", 0); 685 MusicUtils.addToPlaylist(this, list, playlist); 686 return true; 687 } 688 689 case USE_AS_RINGTONE: 690 // Set the system setting to make this the current ringtone 691 MusicUtils.setRingtone(this, mSelectedId); 692 return true; 693 694 case DELETE_ITEM: { 695 long [] list = new long[1]; 696 list[0] = (int) mSelectedId; 697 Bundle b = new Bundle(); 698 String f = getString(R.string.delete_song_desc); 699 String desc = String.format(f, mCurrentTrackName); 700 b.putString("description", desc); 701 b.putLongArray("items", list); 702 Intent intent = new Intent(); 703 intent.setClass(this, DeleteItems.class); 704 intent.putExtras(b); 705 startActivityForResult(intent, -1); 706 return true; 707 } 708 709 case REMOVE: 710 removePlaylistItem(mSelectedPosition); 711 return true; 712 713 case SEARCH: 714 doSearch(); 715 return true; 716 } 717 return super.onContextItemSelected(item); 718 } 719 720 void doSearch() { 721 CharSequence title = null; 722 String query = null; 723 724 Intent i = new Intent(); 725 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH); 726 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 727 728 title = mCurrentTrackName; 729 if (MediaStore.UNKNOWN_STRING.equals(mCurrentArtistNameForAlbum)) { 730 query = mCurrentTrackName; 731 } else { 732 query = mCurrentArtistNameForAlbum + " " + mCurrentTrackName; 733 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum); 734 } 735 if (MediaStore.UNKNOWN_STRING.equals(mCurrentAlbumName)) { 736 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName); 737 } 738 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, "audio/*"); 739 title = getString(R.string.mediasearch, title); 740 i.putExtra(SearchManager.QUERY, query); 741 742 startActivity(Intent.createChooser(i, title)); 743 } 744 745 // In order to use alt-up/down as a shortcut for moving the selected item 746 // in the list, we need to override dispatchKeyEvent, not onKeyDown. 747 // (onKeyDown never sees these events, since they are handled by the list) 748 @Override 749 public boolean dispatchKeyEvent(KeyEvent event) { 750 if (mPlaylist != null && event.getMetaState() != 0 && 751 event.getAction() == KeyEvent.ACTION_DOWN) { 752 switch (event.getKeyCode()) { 753 case KeyEvent.KEYCODE_DPAD_UP: 754 moveItem(true); 755 return true; 756 case KeyEvent.KEYCODE_DPAD_DOWN: 757 moveItem(false); 758 return true; 759 case KeyEvent.KEYCODE_DEL: 760 removeItem(); 761 return true; 762 } 763 } 764 765 return super.dispatchKeyEvent(event); 766 } 767 768 private void removeItem() { 769 int curcount = mTrackCursor.getCount(); 770 int curpos = mTrackList.getSelectedItemPosition(); 771 if (curcount == 0 || curpos < 0) { 772 return; 773 } 774 775 if ("nowplaying".equals(mPlaylist)) { 776 // remove track from queue 777 778 // Work around bug 902971. To get quick visual feedback 779 // of the deletion of the item, hide the selected view. 780 try { 781 if (curpos != MusicUtils.sService.getQueuePosition()) { 782 mDeletedOneRow = true; 783 } 784 } catch (RemoteException ex) { 785 } 786 View v = mTrackList.getSelectedView(); 787 v.setVisibility(View.GONE); 788 mTrackList.invalidateViews(); 789 ((NowPlayingCursor)mTrackCursor).removeItem(curpos); 790 v.setVisibility(View.VISIBLE); 791 mTrackList.invalidateViews(); 792 } else { 793 // remove track from playlist 794 int colidx = mTrackCursor.getColumnIndexOrThrow( 795 MediaStore.Audio.Playlists.Members._ID); 796 mTrackCursor.moveToPosition(curpos); 797 long id = mTrackCursor.getLong(colidx); 798 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 799 Long.valueOf(mPlaylist)); 800 getContentResolver().delete( 801 ContentUris.withAppendedId(uri, id), null, null); 802 curcount--; 803 if (curcount == 0) { 804 finish(); 805 } else { 806 mTrackList.setSelection(curpos < curcount ? curpos : curcount); 807 } 808 } 809 } 810 811 private void moveItem(boolean up) { 812 int curcount = mTrackCursor.getCount(); 813 int curpos = mTrackList.getSelectedItemPosition(); 814 if ( (up && curpos < 1) || (!up && curpos >= curcount - 1)) { 815 return; 816 } 817 818 if (mTrackCursor instanceof NowPlayingCursor) { 819 NowPlayingCursor c = (NowPlayingCursor) mTrackCursor; 820 c.moveItem(curpos, up ? curpos - 1 : curpos + 1); 821 ((TrackListAdapter)getListAdapter()).notifyDataSetChanged(); 822 getListView().invalidateViews(); 823 mDeletedOneRow = true; 824 if (up) { 825 mTrackList.setSelection(curpos - 1); 826 } else { 827 mTrackList.setSelection(curpos + 1); 828 } 829 } else { 830 int colidx = mTrackCursor.getColumnIndexOrThrow( 831 MediaStore.Audio.Playlists.Members.PLAY_ORDER); 832 mTrackCursor.moveToPosition(curpos); 833 int currentplayidx = mTrackCursor.getInt(colidx); 834 Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external", 835 Long.valueOf(mPlaylist)); 836 ContentValues values = new ContentValues(); 837 String where = MediaStore.Audio.Playlists.Members._ID + "=?"; 838 String [] wherearg = new String[1]; 839 ContentResolver res = getContentResolver(); 840 if (up) { 841 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx - 1); 842 wherearg[0] = mTrackCursor.getString(0); 843 res.update(baseUri, values, where, wherearg); 844 mTrackCursor.moveToPrevious(); 845 } else { 846 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx + 1); 847 wherearg[0] = mTrackCursor.getString(0); 848 res.update(baseUri, values, where, wherearg); 849 mTrackCursor.moveToNext(); 850 } 851 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx); 852 wherearg[0] = mTrackCursor.getString(0); 853 res.update(baseUri, values, where, wherearg); 854 } 855 } 856 857 @Override 858 protected void onListItemClick(ListView l, View v, int position, long id) 859 { 860 if (mTrackCursor.getCount() == 0) { 861 return; 862 } 863 // When selecting a track from the queue, just jump there instead of 864 // reloading the queue. This is both faster, and prevents accidentally 865 // dropping out of party shuffle. 866 if (mTrackCursor instanceof NowPlayingCursor) { 867 if (MusicUtils.sService != null) { 868 try { 869 MusicUtils.sService.setQueuePosition(position); 870 return; 871 } catch (RemoteException ex) { 872 } 873 } 874 } 875 MusicUtils.playAll(this, mTrackCursor, position); 876 } 877 878 @Override 879 public boolean onCreateOptionsMenu(Menu menu) { 880 /* This activity is used for a number of different browsing modes, and the menu can 881 * be different for each of them: 882 * - all tracks, optionally restricted to an album, artist or playlist 883 * - the list of currently playing songs 884 */ 885 super.onCreateOptionsMenu(menu); 886 if (mPlaylist == null) { 887 menu.add(0, PLAY_ALL, 0, R.string.play_all).setIcon(R.drawable.ic_menu_play_clip); 888 } 889 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 890 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle); 891 if (mPlaylist != null) { 892 menu.add(0, SAVE_AS_PLAYLIST, 0, R.string.save_as_playlist).setIcon(android.R.drawable.ic_menu_save); 893 if (mPlaylist.equals("nowplaying")) { 894 menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(R.drawable.ic_menu_clear_playlist); 895 } 896 } 897 return true; 898 } 899 900 @Override 901 public boolean onPrepareOptionsMenu(Menu menu) { 902 MusicUtils.setPartyShuffleMenuIcon(menu); 903 return super.onPrepareOptionsMenu(menu); 904 } 905 906 @Override 907 public boolean onOptionsItemSelected(MenuItem item) { 908 Intent intent; 909 Cursor cursor; 910 switch (item.getItemId()) { 911 case PLAY_ALL: { 912 MusicUtils.playAll(this, mTrackCursor); 913 return true; 914 } 915 916 case PARTY_SHUFFLE: 917 MusicUtils.togglePartyShuffle(); 918 break; 919 920 case SHUFFLE_ALL: 921 // Should 'shuffle all' shuffle ALL, or only the tracks shown? 922 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 923 new String [] { MediaStore.Audio.Media._ID}, 924 MediaStore.Audio.Media.IS_MUSIC + "=1", null, 925 MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 926 if (cursor != null) { 927 MusicUtils.shuffleAll(this, cursor); 928 cursor.close(); 929 } 930 return true; 931 932 case SAVE_AS_PLAYLIST: 933 intent = new Intent(); 934 intent.setClass(this, CreatePlaylist.class); 935 startActivityForResult(intent, SAVE_AS_PLAYLIST); 936 return true; 937 938 case CLEAR_PLAYLIST: 939 // We only clear the current playlist 940 MusicUtils.clearQueue(); 941 return true; 942 } 943 return super.onOptionsItemSelected(item); 944 } 945 946 @Override 947 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 948 switch (requestCode) { 949 case SCAN_DONE: 950 if (resultCode == RESULT_CANCELED) { 951 finish(); 952 } else { 953 getTrackCursor(mAdapter.getQueryHandler(), null, true); 954 } 955 break; 956 957 case NEW_PLAYLIST: 958 if (resultCode == RESULT_OK) { 959 Uri uri = intent.getData(); 960 if (uri != null) { 961 long [] list = new long[] { mSelectedId }; 962 MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment())); 963 } 964 } 965 break; 966 967 case SAVE_AS_PLAYLIST: 968 if (resultCode == RESULT_OK) { 969 Uri uri = intent.getData(); 970 if (uri != null) { 971 long [] list = MusicUtils.getSongListForCursor(mTrackCursor); 972 int plid = Integer.parseInt(uri.getLastPathSegment()); 973 MusicUtils.addToPlaylist(this, list, plid); 974 } 975 } 976 break; 977 } 978 } 979 980 private Cursor getTrackCursor(TrackListAdapter.TrackQueryHandler queryhandler, String filter, 981 boolean async) { 982 983 if (queryhandler == null) { 984 throw new IllegalArgumentException(); 985 } 986 987 Cursor ret = null; 988 mSortOrder = MediaStore.Audio.Media.TITLE_KEY; 989 StringBuilder where = new StringBuilder(); 990 where.append(MediaStore.Audio.Media.TITLE + " != ''"); 991 992 // Add in the filtering constraints 993 String [] keywords = null; 994 if (filter != null) { 995 String [] searchWords = filter.split(" "); 996 keywords = new String[searchWords.length]; 997 Collator col = Collator.getInstance(); 998 col.setStrength(Collator.PRIMARY); 999 for (int i = 0; i < searchWords.length; i++) { 1000 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 1001 } 1002 for (int i = 0; i < searchWords.length; i++) { 1003 where.append(" AND "); 1004 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||"); 1005 where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?"); 1006 } 1007 } 1008 1009 if (mGenre != null) { 1010 mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER; 1011 ret = queryhandler.doQuery(MediaStore.Audio.Genres.Members.getContentUri("external", 1012 Integer.valueOf(mGenre)), 1013 mCursorCols, where.toString(), keywords, mSortOrder, async); 1014 } else if (mPlaylist != null) { 1015 if (mPlaylist.equals("nowplaying")) { 1016 if (MusicUtils.sService != null) { 1017 ret = new NowPlayingCursor(MusicUtils.sService, mCursorCols); 1018 if (ret.getCount() == 0) { 1019 finish(); 1020 } 1021 } else { 1022 // Nothing is playing. 1023 } 1024 } else if (mPlaylist.equals("podcasts")) { 1025 where.append(" AND " + MediaStore.Audio.Media.IS_PODCAST + "=1"); 1026 ret = queryhandler.doQuery(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1027 mCursorCols, where.toString(), keywords, 1028 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1029 } else if (mPlaylist.equals("recentlyadded")) { 1030 // do a query for all songs added in the last X weeks 1031 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7); 1032 where.append(" AND " + MediaStore.MediaColumns.DATE_ADDED + ">"); 1033 where.append(System.currentTimeMillis() / 1000 - X); 1034 ret = queryhandler.doQuery(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1035 mCursorCols, where.toString(), keywords, 1036 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1037 } else { 1038 mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER; 1039 ret = queryhandler.doQuery(MediaStore.Audio.Playlists.Members.getContentUri("external", 1040 Long.valueOf(mPlaylist)), mPlaylistMemberCols, 1041 where.toString(), keywords, mSortOrder, async); 1042 } 1043 } else { 1044 if (mAlbumId != null) { 1045 where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "=" + mAlbumId); 1046 mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder; 1047 } 1048 if (mArtistId != null) { 1049 where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId); 1050 } 1051 where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1"); 1052 ret = queryhandler.doQuery(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1053 mCursorCols, where.toString() , keywords, mSortOrder, async); 1054 } 1055 1056 // This special case is for the "nowplaying" cursor, which cannot be handled 1057 // asynchronously using AsyncQueryHandler, so we do some extra initialization here. 1058 if (ret != null && async) { 1059 init(ret, false); 1060 setTitle(); 1061 } 1062 return ret; 1063 } 1064 1065 private class NowPlayingCursor extends AbstractCursor 1066 { 1067 public NowPlayingCursor(IMediaPlaybackService service, String [] cols) 1068 { 1069 mCols = cols; 1070 mService = service; 1071 makeNowPlayingCursor(); 1072 } 1073 private void makeNowPlayingCursor() { 1074 mCurrentPlaylistCursor = null; 1075 try { 1076 mNowPlaying = mService.getQueue(); 1077 } catch (RemoteException ex) { 1078 mNowPlaying = new long[0]; 1079 } 1080 mSize = mNowPlaying.length; 1081 if (mSize == 0) { 1082 return; 1083 } 1084 1085 StringBuilder where = new StringBuilder(); 1086 where.append(MediaStore.Audio.Media._ID + " IN ("); 1087 for (int i = 0; i < mSize; i++) { 1088 where.append(mNowPlaying[i]); 1089 if (i < mSize - 1) { 1090 where.append(","); 1091 } 1092 } 1093 where.append(")"); 1094 1095 mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this, 1096 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1097 mCols, where.toString(), null, MediaStore.Audio.Media._ID); 1098 1099 if (mCurrentPlaylistCursor == null) { 1100 mSize = 0; 1101 return; 1102 } 1103 1104 int size = mCurrentPlaylistCursor.getCount(); 1105 mCursorIdxs = new long[size]; 1106 mCurrentPlaylistCursor.moveToFirst(); 1107 int colidx = mCurrentPlaylistCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1108 for (int i = 0; i < size; i++) { 1109 mCursorIdxs[i] = mCurrentPlaylistCursor.getLong(colidx); 1110 mCurrentPlaylistCursor.moveToNext(); 1111 } 1112 mCurrentPlaylistCursor.moveToFirst(); 1113 mCurPos = -1; 1114 1115 // At this point we can verify the 'now playing' list we got 1116 // earlier to make sure that all the items in there still exist 1117 // in the database, and remove those that aren't. This way we 1118 // don't get any blank items in the list. 1119 try { 1120 int removed = 0; 1121 for (int i = mNowPlaying.length - 1; i >= 0; i--) { 1122 long trackid = mNowPlaying[i]; 1123 int crsridx = Arrays.binarySearch(mCursorIdxs, trackid); 1124 if (crsridx < 0) { 1125 //Log.i("@@@@@", "item no longer exists in db: " + trackid); 1126 removed += mService.removeTrack(trackid); 1127 } 1128 } 1129 if (removed > 0) { 1130 mNowPlaying = mService.getQueue(); 1131 mSize = mNowPlaying.length; 1132 if (mSize == 0) { 1133 mCursorIdxs = null; 1134 return; 1135 } 1136 } 1137 } catch (RemoteException ex) { 1138 mNowPlaying = new long[0]; 1139 } 1140 } 1141 1142 @Override 1143 public int getCount() 1144 { 1145 return mSize; 1146 } 1147 1148 @Override 1149 public boolean onMove(int oldPosition, int newPosition) 1150 { 1151 if (oldPosition == newPosition) 1152 return true; 1153 1154 if (mNowPlaying == null || mCursorIdxs == null) { 1155 return false; 1156 } 1157 1158 // The cursor doesn't have any duplicates in it, and is not ordered 1159 // in queue-order, so we need to figure out where in the cursor we 1160 // should be. 1161 1162 long newid = mNowPlaying[newPosition]; 1163 int crsridx = Arrays.binarySearch(mCursorIdxs, newid); 1164 mCurrentPlaylistCursor.moveToPosition(crsridx); 1165 mCurPos = newPosition; 1166 1167 return true; 1168 } 1169 1170 public boolean removeItem(int which) 1171 { 1172 try { 1173 if (mService.removeTracks(which, which) == 0) { 1174 return false; // delete failed 1175 } 1176 int i = (int) which; 1177 mSize--; 1178 while (i < mSize) { 1179 mNowPlaying[i] = mNowPlaying[i+1]; 1180 i++; 1181 } 1182 onMove(-1, (int) mCurPos); 1183 } catch (RemoteException ex) { 1184 } 1185 return true; 1186 } 1187 1188 public void moveItem(int from, int to) { 1189 try { 1190 mService.moveQueueItem(from, to); 1191 mNowPlaying = mService.getQueue(); 1192 onMove(-1, mCurPos); // update the underlying cursor 1193 } catch (RemoteException ex) { 1194 } 1195 } 1196 1197 private void dump() { 1198 String where = "("; 1199 for (int i = 0; i < mSize; i++) { 1200 where += mNowPlaying[i]; 1201 if (i < mSize - 1) { 1202 where += ","; 1203 } 1204 } 1205 where += ")"; 1206 Log.i("NowPlayingCursor: ", where); 1207 } 1208 1209 @Override 1210 public String getString(int column) 1211 { 1212 try { 1213 return mCurrentPlaylistCursor.getString(column); 1214 } catch (Exception ex) { 1215 onChange(true); 1216 return ""; 1217 } 1218 } 1219 1220 @Override 1221 public short getShort(int column) 1222 { 1223 return mCurrentPlaylistCursor.getShort(column); 1224 } 1225 1226 @Override 1227 public int getInt(int column) 1228 { 1229 try { 1230 return mCurrentPlaylistCursor.getInt(column); 1231 } catch (Exception ex) { 1232 onChange(true); 1233 return 0; 1234 } 1235 } 1236 1237 @Override 1238 public long getLong(int column) 1239 { 1240 try { 1241 return mCurrentPlaylistCursor.getLong(column); 1242 } catch (Exception ex) { 1243 onChange(true); 1244 return 0; 1245 } 1246 } 1247 1248 @Override 1249 public float getFloat(int column) 1250 { 1251 return mCurrentPlaylistCursor.getFloat(column); 1252 } 1253 1254 @Override 1255 public double getDouble(int column) 1256 { 1257 return mCurrentPlaylistCursor.getDouble(column); 1258 } 1259 1260 @Override 1261 public boolean isNull(int column) 1262 { 1263 return mCurrentPlaylistCursor.isNull(column); 1264 } 1265 1266 @Override 1267 public String[] getColumnNames() 1268 { 1269 return mCols; 1270 } 1271 1272 @Override 1273 public void deactivate() 1274 { 1275 if (mCurrentPlaylistCursor != null) 1276 mCurrentPlaylistCursor.deactivate(); 1277 } 1278 1279 @Override 1280 public boolean requery() 1281 { 1282 makeNowPlayingCursor(); 1283 return true; 1284 } 1285 1286 private String [] mCols; 1287 private Cursor mCurrentPlaylistCursor; // updated in onMove 1288 private int mSize; // size of the queue 1289 private long[] mNowPlaying; 1290 private long[] mCursorIdxs; 1291 private int mCurPos; 1292 private IMediaPlaybackService mService; 1293 } 1294 1295 static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer { 1296 boolean mIsNowPlaying; 1297 boolean mDisableNowPlayingIndicator; 1298 1299 int mTitleIdx; 1300 int mArtistIdx; 1301 int mDurationIdx; 1302 int mAudioIdIdx; 1303 1304 private final StringBuilder mBuilder = new StringBuilder(); 1305 private final String mUnknownArtist; 1306 private final String mUnknownAlbum; 1307 1308 private AlphabetIndexer mIndexer; 1309 1310 private TrackBrowserActivity mActivity = null; 1311 private TrackQueryHandler mQueryHandler; 1312 private String mConstraint = null; 1313 private boolean mConstraintIsValid = false; 1314 1315 static class ViewHolder { 1316 TextView line1; 1317 TextView line2; 1318 TextView duration; 1319 ImageView play_indicator; 1320 CharArrayBuffer buffer1; 1321 char [] buffer2; 1322 } 1323 1324 class TrackQueryHandler extends AsyncQueryHandler { 1325 1326 class QueryArgs { 1327 public Uri uri; 1328 public String [] projection; 1329 public String selection; 1330 public String [] selectionArgs; 1331 public String orderBy; 1332 } 1333 1334 TrackQueryHandler(ContentResolver res) { 1335 super(res); 1336 } 1337 1338 public Cursor doQuery(Uri uri, String[] projection, 1339 String selection, String[] selectionArgs, 1340 String orderBy, boolean async) { 1341 if (async) { 1342 // Get 100 results first, which is enough to allow the user to start scrolling, 1343 // while still being very fast. 1344 Uri limituri = uri.buildUpon().appendQueryParameter("limit", "100").build(); 1345 QueryArgs args = new QueryArgs(); 1346 args.uri = uri; 1347 args.projection = projection; 1348 args.selection = selection; 1349 args.selectionArgs = selectionArgs; 1350 args.orderBy = orderBy; 1351 1352 startQuery(0, args, limituri, projection, selection, selectionArgs, orderBy); 1353 return null; 1354 } 1355 return MusicUtils.query(mActivity, 1356 uri, projection, selection, selectionArgs, orderBy); 1357 } 1358 1359 @Override 1360 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 1361 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity); 1362 mActivity.init(cursor, cookie != null); 1363 if (token == 0 && cookie != null && cursor != null && cursor.getCount() >= 100) { 1364 QueryArgs args = (QueryArgs) cookie; 1365 startQuery(1, null, args.uri, args.projection, args.selection, 1366 args.selectionArgs, args.orderBy); 1367 } 1368 } 1369 } 1370 1371 TrackListAdapter(Context context, TrackBrowserActivity currentactivity, 1372 int layout, Cursor cursor, String[] from, int[] to, 1373 boolean isnowplaying, boolean disablenowplayingindicator) { 1374 super(context, layout, cursor, from, to); 1375 mActivity = currentactivity; 1376 getColumnIndices(cursor); 1377 mIsNowPlaying = isnowplaying; 1378 mDisableNowPlayingIndicator = disablenowplayingindicator; 1379 mUnknownArtist = context.getString(R.string.unknown_artist_name); 1380 mUnknownAlbum = context.getString(R.string.unknown_album_name); 1381 1382 mQueryHandler = new TrackQueryHandler(context.getContentResolver()); 1383 } 1384 1385 public void setActivity(TrackBrowserActivity newactivity) { 1386 mActivity = newactivity; 1387 } 1388 1389 public TrackQueryHandler getQueryHandler() { 1390 return mQueryHandler; 1391 } 1392 1393 private void getColumnIndices(Cursor cursor) { 1394 if (cursor != null) { 1395 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); 1396 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); 1397 mDurationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); 1398 try { 1399 mAudioIdIdx = cursor.getColumnIndexOrThrow( 1400 MediaStore.Audio.Playlists.Members.AUDIO_ID); 1401 } catch (IllegalArgumentException ex) { 1402 mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1403 } 1404 1405 if (mIndexer != null) { 1406 mIndexer.setCursor(cursor); 1407 } else if (!mActivity.mEditMode) { 1408 String alpha = mActivity.getString(R.string.fast_scroll_alphabet); 1409 1410 mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha); 1411 } 1412 } 1413 } 1414 1415 @Override 1416 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1417 View v = super.newView(context, cursor, parent); 1418 ImageView iv = (ImageView) v.findViewById(R.id.icon); 1419 if (mActivity.mEditMode) { 1420 iv.setVisibility(View.VISIBLE); 1421 iv.setImageResource(R.drawable.ic_mp_move); 1422 } else { 1423 iv.setVisibility(View.GONE); 1424 } 1425 1426 ViewHolder vh = new ViewHolder(); 1427 vh.line1 = (TextView) v.findViewById(R.id.line1); 1428 vh.line2 = (TextView) v.findViewById(R.id.line2); 1429 vh.duration = (TextView) v.findViewById(R.id.duration); 1430 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator); 1431 vh.buffer1 = new CharArrayBuffer(100); 1432 vh.buffer2 = new char[200]; 1433 v.setTag(vh); 1434 return v; 1435 } 1436 1437 @Override 1438 public void bindView(View view, Context context, Cursor cursor) { 1439 1440 ViewHolder vh = (ViewHolder) view.getTag(); 1441 1442 cursor.copyStringToBuffer(mTitleIdx, vh.buffer1); 1443 vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied); 1444 1445 int secs = cursor.getInt(mDurationIdx) / 1000; 1446 if (secs == 0) { 1447 vh.duration.setText(""); 1448 } else { 1449 vh.duration.setText(MusicUtils.makeTimeString(context, secs)); 1450 } 1451 1452 final StringBuilder builder = mBuilder; 1453 builder.delete(0, builder.length()); 1454 1455 String name = cursor.getString(mArtistIdx); 1456 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 1457 builder.append(mUnknownArtist); 1458 } else { 1459 builder.append(name); 1460 } 1461 int len = builder.length(); 1462 if (vh.buffer2.length < len) { 1463 vh.buffer2 = new char[len]; 1464 } 1465 builder.getChars(0, len, vh.buffer2, 0); 1466 vh.line2.setText(vh.buffer2, 0, len); 1467 1468 ImageView iv = vh.play_indicator; 1469 long id = -1; 1470 if (MusicUtils.sService != null) { 1471 // TODO: IPC call on each bind?? 1472 try { 1473 if (mIsNowPlaying) { 1474 id = MusicUtils.sService.getQueuePosition(); 1475 } else { 1476 id = MusicUtils.sService.getAudioId(); 1477 } 1478 } catch (RemoteException ex) { 1479 } 1480 } 1481 1482 // Determining whether and where to show the "now playing indicator 1483 // is tricky, because we don't actually keep track of where the songs 1484 // in the current playlist came from after they've started playing. 1485 // 1486 // If the "current playlists" is shown, then we can simply match by position, 1487 // otherwise, we need to match by id. Match-by-id gets a little weird if 1488 // a song appears in a playlist more than once, and you're in edit-playlist 1489 // mode. In that case, both items will have the "now playing" indicator. 1490 // For this reason, we don't show the play indicator at all when in edit 1491 // playlist mode (except when you're viewing the "current playlist", 1492 // which is not really a playlist) 1493 if ( (mIsNowPlaying && cursor.getPosition() == id) || 1494 (!mIsNowPlaying && !mDisableNowPlayingIndicator && cursor.getLong(mAudioIdIdx) == id)) { 1495 iv.setImageResource(R.drawable.indicator_ic_mp_playing_list); 1496 iv.setVisibility(View.VISIBLE); 1497 } else { 1498 iv.setVisibility(View.GONE); 1499 } 1500 } 1501 1502 @Override 1503 public void changeCursor(Cursor cursor) { 1504 if (mActivity.isFinishing() && cursor != null) { 1505 cursor.close(); 1506 cursor = null; 1507 } 1508 if (cursor != mActivity.mTrackCursor) { 1509 mActivity.mTrackCursor = cursor; 1510 super.changeCursor(cursor); 1511 getColumnIndices(cursor); 1512 } 1513 } 1514 1515 @Override 1516 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 1517 String s = constraint.toString(); 1518 if (mConstraintIsValid && ( 1519 (s == null && mConstraint == null) || 1520 (s != null && s.equals(mConstraint)))) { 1521 return getCursor(); 1522 } 1523 Cursor c = mActivity.getTrackCursor(mQueryHandler, s, false); 1524 mConstraint = s; 1525 mConstraintIsValid = true; 1526 return c; 1527 } 1528 1529 // SectionIndexer methods 1530 1531 public Object[] getSections() { 1532 if (mIndexer != null) { 1533 return mIndexer.getSections(); 1534 } else { 1535 return null; 1536 } 1537 } 1538 1539 public int getPositionForSection(int section) { 1540 int pos = mIndexer.getPositionForSection(section); 1541 return pos; 1542 } 1543 1544 public int getSectionForPosition(int position) { 1545 return 0; 1546 } 1547 } 1548} 1549 1550