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 int curpos = mTrackList.getSelectedItemPosition(); 777 if (mPlaylist != null && !mPlaylist.equals("recentlyadded") && curpos >= 0 && 778 event.getMetaState() != 0 && event.getAction() == KeyEvent.ACTION_DOWN) { 779 switch (event.getKeyCode()) { 780 case KeyEvent.KEYCODE_DPAD_UP: 781 moveItem(true); 782 return true; 783 case KeyEvent.KEYCODE_DPAD_DOWN: 784 moveItem(false); 785 return true; 786 case KeyEvent.KEYCODE_DEL: 787 removeItem(); 788 return true; 789 } 790 } 791 792 return super.dispatchKeyEvent(event); 793 } 794 795 private void removeItem() { 796 int curcount = mTrackCursor.getCount(); 797 int curpos = mTrackList.getSelectedItemPosition(); 798 if (curcount == 0 || curpos < 0) { 799 return; 800 } 801 802 if ("nowplaying".equals(mPlaylist)) { 803 // remove track from queue 804 805 // Work around bug 902971. To get quick visual feedback 806 // of the deletion of the item, hide the selected view. 807 try { 808 if (curpos != MusicUtils.sService.getQueuePosition()) { 809 mDeletedOneRow = true; 810 } 811 } catch (RemoteException ex) { 812 } 813 View v = mTrackList.getSelectedView(); 814 v.setVisibility(View.GONE); 815 mTrackList.invalidateViews(); 816 ((NowPlayingCursor)mTrackCursor).removeItem(curpos); 817 v.setVisibility(View.VISIBLE); 818 mTrackList.invalidateViews(); 819 } else { 820 // remove track from playlist 821 int colidx = mTrackCursor.getColumnIndexOrThrow( 822 MediaStore.Audio.Playlists.Members._ID); 823 mTrackCursor.moveToPosition(curpos); 824 long id = mTrackCursor.getLong(colidx); 825 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 826 Long.valueOf(mPlaylist)); 827 getContentResolver().delete( 828 ContentUris.withAppendedId(uri, id), null, null); 829 curcount--; 830 if (curcount == 0) { 831 finish(); 832 } else { 833 mTrackList.setSelection(curpos < curcount ? curpos : curcount); 834 } 835 } 836 } 837 838 private void moveItem(boolean up) { 839 int curcount = mTrackCursor.getCount(); 840 int curpos = mTrackList.getSelectedItemPosition(); 841 if ( (up && curpos < 1) || (!up && curpos >= curcount - 1)) { 842 return; 843 } 844 845 if (mTrackCursor instanceof NowPlayingCursor) { 846 NowPlayingCursor c = (NowPlayingCursor) mTrackCursor; 847 c.moveItem(curpos, up ? curpos - 1 : curpos + 1); 848 ((TrackListAdapter)getListAdapter()).notifyDataSetChanged(); 849 getListView().invalidateViews(); 850 mDeletedOneRow = true; 851 if (up) { 852 mTrackList.setSelection(curpos - 1); 853 } else { 854 mTrackList.setSelection(curpos + 1); 855 } 856 } else { 857 int colidx = mTrackCursor.getColumnIndexOrThrow( 858 MediaStore.Audio.Playlists.Members.PLAY_ORDER); 859 mTrackCursor.moveToPosition(curpos); 860 int currentplayidx = mTrackCursor.getInt(colidx); 861 Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external", 862 Long.valueOf(mPlaylist)); 863 ContentValues values = new ContentValues(); 864 String where = MediaStore.Audio.Playlists.Members._ID + "=?"; 865 String [] wherearg = new String[1]; 866 ContentResolver res = getContentResolver(); 867 if (up) { 868 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx - 1); 869 wherearg[0] = mTrackCursor.getString(0); 870 res.update(baseUri, values, where, wherearg); 871 mTrackCursor.moveToPrevious(); 872 } else { 873 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx + 1); 874 wherearg[0] = mTrackCursor.getString(0); 875 res.update(baseUri, values, where, wherearg); 876 mTrackCursor.moveToNext(); 877 } 878 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx); 879 wherearg[0] = mTrackCursor.getString(0); 880 res.update(baseUri, values, where, wherearg); 881 } 882 } 883 884 @Override 885 protected void onListItemClick(ListView l, View v, int position, long id) 886 { 887 if (mTrackCursor.getCount() == 0) { 888 return; 889 } 890 // When selecting a track from the queue, just jump there instead of 891 // reloading the queue. This is both faster, and prevents accidentally 892 // dropping out of party shuffle. 893 if (mTrackCursor instanceof NowPlayingCursor) { 894 if (MusicUtils.sService != null) { 895 try { 896 MusicUtils.sService.setQueuePosition(position); 897 return; 898 } catch (RemoteException ex) { 899 } 900 } 901 } 902 MusicUtils.playAll(this, mTrackCursor, position); 903 } 904 905 @Override 906 public boolean onCreateOptionsMenu(Menu menu) { 907 /* This activity is used for a number of different browsing modes, and the menu can 908 * be different for each of them: 909 * - all tracks, optionally restricted to an album, artist or playlist 910 * - the list of currently playing songs 911 */ 912 super.onCreateOptionsMenu(menu); 913 if (mPlaylist == null) { 914 menu.add(0, PLAY_ALL, 0, R.string.play_all).setIcon(R.drawable.ic_menu_play_clip); 915 } 916 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 917 menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle); 918 if (mPlaylist != null) { 919 menu.add(0, SAVE_AS_PLAYLIST, 0, R.string.save_as_playlist).setIcon(android.R.drawable.ic_menu_save); 920 if (mPlaylist.equals("nowplaying")) { 921 menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(R.drawable.ic_menu_clear_playlist); 922 } 923 } 924 return true; 925 } 926 927 @Override 928 public boolean onPrepareOptionsMenu(Menu menu) { 929 MusicUtils.setPartyShuffleMenuIcon(menu); 930 return super.onPrepareOptionsMenu(menu); 931 } 932 933 @Override 934 public boolean onOptionsItemSelected(MenuItem item) { 935 Intent intent; 936 Cursor cursor; 937 switch (item.getItemId()) { 938 case PLAY_ALL: { 939 MusicUtils.playAll(this, mTrackCursor); 940 return true; 941 } 942 943 case PARTY_SHUFFLE: 944 MusicUtils.togglePartyShuffle(); 945 break; 946 947 case SHUFFLE_ALL: 948 // Should 'shuffle all' shuffle ALL, or only the tracks shown? 949 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 950 new String [] { MediaStore.Audio.Media._ID}, 951 MediaStore.Audio.Media.IS_MUSIC + "=1", null, 952 MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 953 if (cursor != null) { 954 MusicUtils.shuffleAll(this, cursor); 955 cursor.close(); 956 } 957 return true; 958 959 case SAVE_AS_PLAYLIST: 960 intent = new Intent(); 961 intent.setClass(this, CreatePlaylist.class); 962 startActivityForResult(intent, SAVE_AS_PLAYLIST); 963 return true; 964 965 case CLEAR_PLAYLIST: 966 // We only clear the current playlist 967 MusicUtils.clearQueue(); 968 return true; 969 } 970 return super.onOptionsItemSelected(item); 971 } 972 973 @Override 974 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 975 switch (requestCode) { 976 case SCAN_DONE: 977 if (resultCode == RESULT_CANCELED) { 978 finish(); 979 } else { 980 getTrackCursor(mAdapter.getQueryHandler(), null, true); 981 } 982 break; 983 984 case NEW_PLAYLIST: 985 if (resultCode == RESULT_OK) { 986 Uri uri = intent.getData(); 987 if (uri != null) { 988 long [] list = new long[] { mSelectedId }; 989 MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment())); 990 } 991 } 992 break; 993 994 case SAVE_AS_PLAYLIST: 995 if (resultCode == RESULT_OK) { 996 Uri uri = intent.getData(); 997 if (uri != null) { 998 long [] list = MusicUtils.getSongListForCursor(mTrackCursor); 999 int plid = Integer.parseInt(uri.getLastPathSegment()); 1000 MusicUtils.addToPlaylist(this, list, plid); 1001 } 1002 } 1003 break; 1004 } 1005 } 1006 1007 private Cursor getTrackCursor(TrackListAdapter.TrackQueryHandler queryhandler, String filter, 1008 boolean async) { 1009 1010 if (queryhandler == null) { 1011 throw new IllegalArgumentException(); 1012 } 1013 1014 Cursor ret = null; 1015 mSortOrder = MediaStore.Audio.Media.TITLE_KEY; 1016 StringBuilder where = new StringBuilder(); 1017 where.append(MediaStore.Audio.Media.TITLE + " != ''"); 1018 1019 if (mGenre != null) { 1020 Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external", 1021 Integer.valueOf(mGenre)); 1022 if (!TextUtils.isEmpty(filter)) { 1023 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1024 } 1025 mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER; 1026 ret = queryhandler.doQuery(uri, 1027 mCursorCols, where.toString(), null, mSortOrder, async); 1028 } else if (mPlaylist != null) { 1029 if (mPlaylist.equals("nowplaying")) { 1030 if (MusicUtils.sService != null) { 1031 ret = new NowPlayingCursor(MusicUtils.sService, mCursorCols); 1032 if (ret.getCount() == 0) { 1033 finish(); 1034 } 1035 } else { 1036 // Nothing is playing. 1037 } 1038 } else if (mPlaylist.equals("podcasts")) { 1039 where.append(" AND " + MediaStore.Audio.Media.IS_PODCAST + "=1"); 1040 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1041 if (!TextUtils.isEmpty(filter)) { 1042 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1043 } 1044 ret = queryhandler.doQuery(uri, 1045 mCursorCols, where.toString(), null, 1046 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1047 } else if (mPlaylist.equals("recentlyadded")) { 1048 // do a query for all songs added in the last X weeks 1049 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1050 if (!TextUtils.isEmpty(filter)) { 1051 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1052 } 1053 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7); 1054 where.append(" AND " + MediaStore.MediaColumns.DATE_ADDED + ">"); 1055 where.append(System.currentTimeMillis() / 1000 - X); 1056 ret = queryhandler.doQuery(uri, 1057 mCursorCols, where.toString(), null, 1058 MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async); 1059 } else { 1060 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", 1061 Long.valueOf(mPlaylist)); 1062 if (!TextUtils.isEmpty(filter)) { 1063 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1064 } 1065 mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER; 1066 ret = queryhandler.doQuery(uri, mPlaylistMemberCols, 1067 where.toString(), null, mSortOrder, async); 1068 } 1069 } else { 1070 if (mAlbumId != null) { 1071 where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "=" + mAlbumId); 1072 mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder; 1073 } 1074 if (mArtistId != null) { 1075 where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId); 1076 } 1077 where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1"); 1078 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 1079 if (!TextUtils.isEmpty(filter)) { 1080 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build(); 1081 } 1082 ret = queryhandler.doQuery(uri, 1083 mCursorCols, where.toString() , null, mSortOrder, async); 1084 } 1085 1086 // This special case is for the "nowplaying" cursor, which cannot be handled 1087 // asynchronously using AsyncQueryHandler, so we do some extra initialization here. 1088 if (ret != null && async) { 1089 init(ret, false); 1090 setTitle(); 1091 } 1092 return ret; 1093 } 1094 1095 private class NowPlayingCursor extends AbstractCursor 1096 { 1097 public NowPlayingCursor(IMediaPlaybackService service, String [] cols) 1098 { 1099 mCols = cols; 1100 mService = service; 1101 makeNowPlayingCursor(); 1102 } 1103 private void makeNowPlayingCursor() { 1104 mCurrentPlaylistCursor = null; 1105 try { 1106 mNowPlaying = mService.getQueue(); 1107 } catch (RemoteException ex) { 1108 mNowPlaying = new long[0]; 1109 } 1110 mSize = mNowPlaying.length; 1111 if (mSize == 0) { 1112 return; 1113 } 1114 1115 StringBuilder where = new StringBuilder(); 1116 where.append(MediaStore.Audio.Media._ID + " IN ("); 1117 for (int i = 0; i < mSize; i++) { 1118 where.append(mNowPlaying[i]); 1119 if (i < mSize - 1) { 1120 where.append(","); 1121 } 1122 } 1123 where.append(")"); 1124 1125 mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this, 1126 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 1127 mCols, where.toString(), null, MediaStore.Audio.Media._ID); 1128 1129 if (mCurrentPlaylistCursor == null) { 1130 mSize = 0; 1131 return; 1132 } 1133 1134 int size = mCurrentPlaylistCursor.getCount(); 1135 mCursorIdxs = new long[size]; 1136 mCurrentPlaylistCursor.moveToFirst(); 1137 int colidx = mCurrentPlaylistCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1138 for (int i = 0; i < size; i++) { 1139 mCursorIdxs[i] = mCurrentPlaylistCursor.getLong(colidx); 1140 mCurrentPlaylistCursor.moveToNext(); 1141 } 1142 mCurrentPlaylistCursor.moveToFirst(); 1143 mCurPos = -1; 1144 1145 // At this point we can verify the 'now playing' list we got 1146 // earlier to make sure that all the items in there still exist 1147 // in the database, and remove those that aren't. This way we 1148 // don't get any blank items in the list. 1149 try { 1150 int removed = 0; 1151 for (int i = mNowPlaying.length - 1; i >= 0; i--) { 1152 long trackid = mNowPlaying[i]; 1153 int crsridx = Arrays.binarySearch(mCursorIdxs, trackid); 1154 if (crsridx < 0) { 1155 //Log.i("@@@@@", "item no longer exists in db: " + trackid); 1156 removed += mService.removeTrack(trackid); 1157 } 1158 } 1159 if (removed > 0) { 1160 mNowPlaying = mService.getQueue(); 1161 mSize = mNowPlaying.length; 1162 if (mSize == 0) { 1163 mCursorIdxs = null; 1164 return; 1165 } 1166 } 1167 } catch (RemoteException ex) { 1168 mNowPlaying = new long[0]; 1169 } 1170 } 1171 1172 @Override 1173 public int getCount() 1174 { 1175 return mSize; 1176 } 1177 1178 @Override 1179 public boolean onMove(int oldPosition, int newPosition) 1180 { 1181 if (oldPosition == newPosition) 1182 return true; 1183 1184 if (mNowPlaying == null || mCursorIdxs == null || newPosition >= mNowPlaying.length) { 1185 return false; 1186 } 1187 1188 // The cursor doesn't have any duplicates in it, and is not ordered 1189 // in queue-order, so we need to figure out where in the cursor we 1190 // should be. 1191 1192 long newid = mNowPlaying[newPosition]; 1193 int crsridx = Arrays.binarySearch(mCursorIdxs, newid); 1194 mCurrentPlaylistCursor.moveToPosition(crsridx); 1195 mCurPos = newPosition; 1196 1197 return true; 1198 } 1199 1200 public boolean removeItem(int which) 1201 { 1202 try { 1203 if (mService.removeTracks(which, which) == 0) { 1204 return false; // delete failed 1205 } 1206 int i = (int) which; 1207 mSize--; 1208 while (i < mSize) { 1209 mNowPlaying[i] = mNowPlaying[i+1]; 1210 i++; 1211 } 1212 onMove(-1, (int) mCurPos); 1213 } catch (RemoteException ex) { 1214 } 1215 return true; 1216 } 1217 1218 public void moveItem(int from, int to) { 1219 try { 1220 mService.moveQueueItem(from, to); 1221 mNowPlaying = mService.getQueue(); 1222 onMove(-1, mCurPos); // update the underlying cursor 1223 } catch (RemoteException ex) { 1224 } 1225 } 1226 1227 private void dump() { 1228 String where = "("; 1229 for (int i = 0; i < mSize; i++) { 1230 where += mNowPlaying[i]; 1231 if (i < mSize - 1) { 1232 where += ","; 1233 } 1234 } 1235 where += ")"; 1236 Log.i("NowPlayingCursor: ", where); 1237 } 1238 1239 @Override 1240 public String getString(int column) 1241 { 1242 try { 1243 return mCurrentPlaylistCursor.getString(column); 1244 } catch (Exception ex) { 1245 onChange(true); 1246 return ""; 1247 } 1248 } 1249 1250 @Override 1251 public short getShort(int column) 1252 { 1253 return mCurrentPlaylistCursor.getShort(column); 1254 } 1255 1256 @Override 1257 public int getInt(int column) 1258 { 1259 try { 1260 return mCurrentPlaylistCursor.getInt(column); 1261 } catch (Exception ex) { 1262 onChange(true); 1263 return 0; 1264 } 1265 } 1266 1267 @Override 1268 public long getLong(int column) 1269 { 1270 try { 1271 return mCurrentPlaylistCursor.getLong(column); 1272 } catch (Exception ex) { 1273 onChange(true); 1274 return 0; 1275 } 1276 } 1277 1278 @Override 1279 public float getFloat(int column) 1280 { 1281 return mCurrentPlaylistCursor.getFloat(column); 1282 } 1283 1284 @Override 1285 public double getDouble(int column) 1286 { 1287 return mCurrentPlaylistCursor.getDouble(column); 1288 } 1289 1290 @Override 1291 public int getType(int column) { 1292 return mCurrentPlaylistCursor.getType(column); 1293 } 1294 1295 @Override 1296 public boolean isNull(int column) 1297 { 1298 return mCurrentPlaylistCursor.isNull(column); 1299 } 1300 1301 @Override 1302 public String[] getColumnNames() 1303 { 1304 return mCols; 1305 } 1306 1307 @Override 1308 public void deactivate() 1309 { 1310 if (mCurrentPlaylistCursor != null) 1311 mCurrentPlaylistCursor.deactivate(); 1312 } 1313 1314 @Override 1315 public boolean requery() 1316 { 1317 makeNowPlayingCursor(); 1318 return true; 1319 } 1320 1321 private String [] mCols; 1322 private Cursor mCurrentPlaylistCursor; // updated in onMove 1323 private int mSize; // size of the queue 1324 private long[] mNowPlaying; 1325 private long[] mCursorIdxs; 1326 private int mCurPos; 1327 private IMediaPlaybackService mService; 1328 } 1329 1330 static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer { 1331 boolean mIsNowPlaying; 1332 boolean mDisableNowPlayingIndicator; 1333 1334 int mTitleIdx; 1335 int mArtistIdx; 1336 int mDurationIdx; 1337 int mAudioIdIdx; 1338 1339 private final StringBuilder mBuilder = new StringBuilder(); 1340 private final String mUnknownArtist; 1341 private final String mUnknownAlbum; 1342 1343 private AlphabetIndexer mIndexer; 1344 1345 private TrackBrowserActivity mActivity = null; 1346 private TrackQueryHandler mQueryHandler; 1347 private String mConstraint = null; 1348 private boolean mConstraintIsValid = false; 1349 1350 static class ViewHolder { 1351 TextView line1; 1352 TextView line2; 1353 TextView duration; 1354 ImageView play_indicator; 1355 CharArrayBuffer buffer1; 1356 char [] buffer2; 1357 } 1358 1359 class TrackQueryHandler extends AsyncQueryHandler { 1360 1361 class QueryArgs { 1362 public Uri uri; 1363 public String [] projection; 1364 public String selection; 1365 public String [] selectionArgs; 1366 public String orderBy; 1367 } 1368 1369 TrackQueryHandler(ContentResolver res) { 1370 super(res); 1371 } 1372 1373 public Cursor doQuery(Uri uri, String[] projection, 1374 String selection, String[] selectionArgs, 1375 String orderBy, boolean async) { 1376 if (async) { 1377 // Get 100 results first, which is enough to allow the user to start scrolling, 1378 // while still being very fast. 1379 Uri limituri = uri.buildUpon().appendQueryParameter("limit", "100").build(); 1380 QueryArgs args = new QueryArgs(); 1381 args.uri = uri; 1382 args.projection = projection; 1383 args.selection = selection; 1384 args.selectionArgs = selectionArgs; 1385 args.orderBy = orderBy; 1386 1387 startQuery(0, args, limituri, projection, selection, selectionArgs, orderBy); 1388 return null; 1389 } 1390 return MusicUtils.query(mActivity, 1391 uri, projection, selection, selectionArgs, orderBy); 1392 } 1393 1394 @Override 1395 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 1396 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity); 1397 mActivity.init(cursor, cookie != null); 1398 if (token == 0 && cookie != null && cursor != null && cursor.getCount() >= 100) { 1399 QueryArgs args = (QueryArgs) cookie; 1400 startQuery(1, null, args.uri, args.projection, args.selection, 1401 args.selectionArgs, args.orderBy); 1402 } 1403 } 1404 } 1405 1406 TrackListAdapter(Context context, TrackBrowserActivity currentactivity, 1407 int layout, Cursor cursor, String[] from, int[] to, 1408 boolean isnowplaying, boolean disablenowplayingindicator) { 1409 super(context, layout, cursor, from, to); 1410 mActivity = currentactivity; 1411 getColumnIndices(cursor); 1412 mIsNowPlaying = isnowplaying; 1413 mDisableNowPlayingIndicator = disablenowplayingindicator; 1414 mUnknownArtist = context.getString(R.string.unknown_artist_name); 1415 mUnknownAlbum = context.getString(R.string.unknown_album_name); 1416 1417 mQueryHandler = new TrackQueryHandler(context.getContentResolver()); 1418 } 1419 1420 public void setActivity(TrackBrowserActivity newactivity) { 1421 mActivity = newactivity; 1422 } 1423 1424 public TrackQueryHandler getQueryHandler() { 1425 return mQueryHandler; 1426 } 1427 1428 private void getColumnIndices(Cursor cursor) { 1429 if (cursor != null) { 1430 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE); 1431 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST); 1432 mDurationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION); 1433 try { 1434 mAudioIdIdx = cursor.getColumnIndexOrThrow( 1435 MediaStore.Audio.Playlists.Members.AUDIO_ID); 1436 } catch (IllegalArgumentException ex) { 1437 mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID); 1438 } 1439 1440 if (mIndexer != null) { 1441 mIndexer.setCursor(cursor); 1442 } else if (!mActivity.mEditMode && mActivity.mAlbumId == null) { 1443 String alpha = mActivity.getString(R.string.fast_scroll_alphabet); 1444 1445 mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha); 1446 } 1447 } 1448 } 1449 1450 @Override 1451 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1452 View v = super.newView(context, cursor, parent); 1453 ImageView iv = (ImageView) v.findViewById(R.id.icon); 1454 iv.setVisibility(View.GONE); 1455 1456 ViewHolder vh = new ViewHolder(); 1457 vh.line1 = (TextView) v.findViewById(R.id.line1); 1458 vh.line2 = (TextView) v.findViewById(R.id.line2); 1459 vh.duration = (TextView) v.findViewById(R.id.duration); 1460 vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator); 1461 vh.buffer1 = new CharArrayBuffer(100); 1462 vh.buffer2 = new char[200]; 1463 v.setTag(vh); 1464 return v; 1465 } 1466 1467 @Override 1468 public void bindView(View view, Context context, Cursor cursor) { 1469 1470 ViewHolder vh = (ViewHolder) view.getTag(); 1471 1472 cursor.copyStringToBuffer(mTitleIdx, vh.buffer1); 1473 vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied); 1474 1475 int secs = cursor.getInt(mDurationIdx) / 1000; 1476 if (secs == 0) { 1477 vh.duration.setText(""); 1478 } else { 1479 vh.duration.setText(MusicUtils.makeTimeString(context, secs)); 1480 } 1481 1482 final StringBuilder builder = mBuilder; 1483 builder.delete(0, builder.length()); 1484 1485 String name = cursor.getString(mArtistIdx); 1486 if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) { 1487 builder.append(mUnknownArtist); 1488 } else { 1489 builder.append(name); 1490 } 1491 int len = builder.length(); 1492 if (vh.buffer2.length < len) { 1493 vh.buffer2 = new char[len]; 1494 } 1495 builder.getChars(0, len, vh.buffer2, 0); 1496 vh.line2.setText(vh.buffer2, 0, len); 1497 1498 ImageView iv = vh.play_indicator; 1499 long id = -1; 1500 if (MusicUtils.sService != null) { 1501 // TODO: IPC call on each bind?? 1502 try { 1503 if (mIsNowPlaying) { 1504 id = MusicUtils.sService.getQueuePosition(); 1505 } else { 1506 id = MusicUtils.sService.getAudioId(); 1507 } 1508 } catch (RemoteException ex) { 1509 } 1510 } 1511 1512 // Determining whether and where to show the "now playing indicator 1513 // is tricky, because we don't actually keep track of where the songs 1514 // in the current playlist came from after they've started playing. 1515 // 1516 // If the "current playlists" is shown, then we can simply match by position, 1517 // otherwise, we need to match by id. Match-by-id gets a little weird if 1518 // a song appears in a playlist more than once, and you're in edit-playlist 1519 // mode. In that case, both items will have the "now playing" indicator. 1520 // For this reason, we don't show the play indicator at all when in edit 1521 // playlist mode (except when you're viewing the "current playlist", 1522 // which is not really a playlist) 1523 if ( (mIsNowPlaying && cursor.getPosition() == id) || 1524 (!mIsNowPlaying && !mDisableNowPlayingIndicator && cursor.getLong(mAudioIdIdx) == id)) { 1525 iv.setImageResource(R.drawable.indicator_ic_mp_playing_list); 1526 iv.setVisibility(View.VISIBLE); 1527 } else { 1528 iv.setVisibility(View.GONE); 1529 } 1530 } 1531 1532 @Override 1533 public void changeCursor(Cursor cursor) { 1534 if (mActivity.isFinishing() && cursor != null) { 1535 cursor.close(); 1536 cursor = null; 1537 } 1538 if (cursor != mActivity.mTrackCursor) { 1539 mActivity.mTrackCursor = cursor; 1540 super.changeCursor(cursor); 1541 getColumnIndices(cursor); 1542 } 1543 } 1544 1545 @Override 1546 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 1547 String s = constraint.toString(); 1548 if (mConstraintIsValid && ( 1549 (s == null && mConstraint == null) || 1550 (s != null && s.equals(mConstraint)))) { 1551 return getCursor(); 1552 } 1553 Cursor c = mActivity.getTrackCursor(mQueryHandler, s, false); 1554 mConstraint = s; 1555 mConstraintIsValid = true; 1556 return c; 1557 } 1558 1559 // SectionIndexer methods 1560 1561 public Object[] getSections() { 1562 if (mIndexer != null) { 1563 return mIndexer.getSections(); 1564 } else { 1565 return new String [] { " " }; 1566 } 1567 } 1568 1569 public int getPositionForSection(int section) { 1570 if (mIndexer != null) { 1571 return mIndexer.getPositionForSection(section); 1572 } 1573 return 0; 1574 } 1575 1576 public int getSectionForPosition(int position) { 1577 return 0; 1578 } 1579 } 1580 } 1581 1582