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