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