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