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