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