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