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