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