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