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         if (mPlaylist != null && event.getMetaState() != 0 &&
    777                 event.getAction() == KeyEvent.ACTION_DOWN) {
    778             switch (event.getKeyCode()) {
    779                 case KeyEvent.KEYCODE_DPAD_UP:
    780                     moveItem(true);
    781                     return true;
    782                 case KeyEvent.KEYCODE_DPAD_DOWN:
    783                     moveItem(false);
    784                     return true;
    785                 case KeyEvent.KEYCODE_DEL:
    786                     removeItem();
    787                     return true;
    788             }
    789         }
    790 
    791         return super.dispatchKeyEvent(event);
    792     }
    793 
    794     private void removeItem() {
    795         int curcount = mTrackCursor.getCount();
    796         int curpos = mTrackList.getSelectedItemPosition();
    797         if (curcount == 0 || curpos < 0) {
    798             return;
    799         }
    800 
    801         if ("nowplaying".equals(mPlaylist)) {
    802             // remove track from queue
    803 
    804             // Work around bug 902971. To get quick visual feedback
    805             // of the deletion of the item, hide the selected view.
    806             try {
    807                 if (curpos != MusicUtils.sService.getQueuePosition()) {
    808                     mDeletedOneRow = true;
    809                 }
    810             } catch (RemoteException ex) {
    811             }
    812             View v = mTrackList.getSelectedView();
    813             v.setVisibility(View.GONE);
    814             mTrackList.invalidateViews();
    815             ((NowPlayingCursor)mTrackCursor).removeItem(curpos);
    816             v.setVisibility(View.VISIBLE);
    817             mTrackList.invalidateViews();
    818         } else {
    819             // remove track from playlist
    820             int colidx = mTrackCursor.getColumnIndexOrThrow(
    821                     MediaStore.Audio.Playlists.Members._ID);
    822             mTrackCursor.moveToPosition(curpos);
    823             long id = mTrackCursor.getLong(colidx);
    824             Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
    825                     Long.valueOf(mPlaylist));
    826             getContentResolver().delete(
    827                     ContentUris.withAppendedId(uri, id), null, null);
    828             curcount--;
    829             if (curcount == 0) {
    830                 finish();
    831             } else {
    832                 mTrackList.setSelection(curpos < curcount ? curpos : curcount);
    833             }
    834         }
    835     }
    836 
    837     private void moveItem(boolean up) {
    838         int curcount = mTrackCursor.getCount();
    839         int curpos = mTrackList.getSelectedItemPosition();
    840         if ( (up && curpos < 1) || (!up  && curpos >= curcount - 1)) {
    841             return;
    842         }
    843 
    844         if (mTrackCursor instanceof NowPlayingCursor) {
    845             NowPlayingCursor c = (NowPlayingCursor) mTrackCursor;
    846             c.moveItem(curpos, up ? curpos - 1 : curpos + 1);
    847             ((TrackListAdapter)getListAdapter()).notifyDataSetChanged();
    848             getListView().invalidateViews();
    849             mDeletedOneRow = true;
    850             if (up) {
    851                 mTrackList.setSelection(curpos - 1);
    852             } else {
    853                 mTrackList.setSelection(curpos + 1);
    854             }
    855         } else {
    856             int colidx = mTrackCursor.getColumnIndexOrThrow(
    857                     MediaStore.Audio.Playlists.Members.PLAY_ORDER);
    858             mTrackCursor.moveToPosition(curpos);
    859             int currentplayidx = mTrackCursor.getInt(colidx);
    860             Uri baseUri = MediaStore.Audio.Playlists.Members.getContentUri("external",
    861                     Long.valueOf(mPlaylist));
    862             ContentValues values = new ContentValues();
    863             String where = MediaStore.Audio.Playlists.Members._ID + "=?";
    864             String [] wherearg = new String[1];
    865             ContentResolver res = getContentResolver();
    866             if (up) {
    867                 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx - 1);
    868                 wherearg[0] = mTrackCursor.getString(0);
    869                 res.update(baseUri, values, where, wherearg);
    870                 mTrackCursor.moveToPrevious();
    871             } else {
    872                 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx + 1);
    873                 wherearg[0] = mTrackCursor.getString(0);
    874                 res.update(baseUri, values, where, wherearg);
    875                 mTrackCursor.moveToNext();
    876             }
    877             values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx);
    878             wherearg[0] = mTrackCursor.getString(0);
    879             res.update(baseUri, values, where, wherearg);
    880         }
    881     }
    882 
    883     @Override
    884     protected void onListItemClick(ListView l, View v, int position, long id)
    885     {
    886         if (mTrackCursor.getCount() == 0) {
    887             return;
    888         }
    889         // When selecting a track from the queue, just jump there instead of
    890         // reloading the queue. This is both faster, and prevents accidentally
    891         // dropping out of party shuffle.
    892         if (mTrackCursor instanceof NowPlayingCursor) {
    893             if (MusicUtils.sService != null) {
    894                 try {
    895                     MusicUtils.sService.setQueuePosition(position);
    896                     return;
    897                 } catch (RemoteException ex) {
    898                 }
    899             }
    900         }
    901         MusicUtils.playAll(this, mTrackCursor, position);
    902     }
    903 
    904     @Override
    905     public boolean onCreateOptionsMenu(Menu menu) {
    906         /* This activity is used for a number of different browsing modes, and the menu can
    907          * be different for each of them:
    908          * - all tracks, optionally restricted to an album, artist or playlist
    909          * - the list of currently playing songs
    910          */
    911         super.onCreateOptionsMenu(menu);
    912         if (mPlaylist == null) {
    913             menu.add(0, PLAY_ALL, 0, R.string.play_all).setIcon(R.drawable.ic_menu_play_clip);
    914         }
    915         menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
    916         menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
    917         if (mPlaylist != null) {
    918             menu.add(0, SAVE_AS_PLAYLIST, 0, R.string.save_as_playlist).setIcon(android.R.drawable.ic_menu_save);
    919             if (mPlaylist.equals("nowplaying")) {
    920                 menu.add(0, CLEAR_PLAYLIST, 0, R.string.clear_playlist).setIcon(R.drawable.ic_menu_clear_playlist);
    921             }
    922         }
    923         return true;
    924     }
    925 
    926     @Override
    927     public boolean onPrepareOptionsMenu(Menu menu) {
    928         MusicUtils.setPartyShuffleMenuIcon(menu);
    929         return super.onPrepareOptionsMenu(menu);
    930     }
    931 
    932     @Override
    933     public boolean onOptionsItemSelected(MenuItem item) {
    934         Intent intent;
    935         Cursor cursor;
    936         switch (item.getItemId()) {
    937             case PLAY_ALL: {
    938                 MusicUtils.playAll(this, mTrackCursor);
    939                 return true;
    940             }
    941 
    942             case PARTY_SHUFFLE:
    943                 MusicUtils.togglePartyShuffle();
    944                 break;
    945 
    946             case SHUFFLE_ALL:
    947                 // Should 'shuffle all' shuffle ALL, or only the tracks shown?
    948                 cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
    949                         new String [] { MediaStore.Audio.Media._ID},
    950                         MediaStore.Audio.Media.IS_MUSIC + "=1", null,
    951                         MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
    952                 if (cursor != null) {
    953                     MusicUtils.shuffleAll(this, cursor);
    954                     cursor.close();
    955                 }
    956                 return true;
    957 
    958             case SAVE_AS_PLAYLIST:
    959                 intent = new Intent();
    960                 intent.setClass(this, CreatePlaylist.class);
    961                 startActivityForResult(intent, SAVE_AS_PLAYLIST);
    962                 return true;
    963 
    964             case CLEAR_PLAYLIST:
    965                 // We only clear the current playlist
    966                 MusicUtils.clearQueue();
    967                 return true;
    968         }
    969         return super.onOptionsItemSelected(item);
    970     }
    971 
    972     @Override
    973     protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    974         switch (requestCode) {
    975             case SCAN_DONE:
    976                 if (resultCode == RESULT_CANCELED) {
    977                     finish();
    978                 } else {
    979                     getTrackCursor(mAdapter.getQueryHandler(), null, true);
    980                 }
    981                 break;
    982 
    983             case NEW_PLAYLIST:
    984                 if (resultCode == RESULT_OK) {
    985                     Uri uri = intent.getData();
    986                     if (uri != null) {
    987                         long [] list = new long[] { mSelectedId };
    988                         MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment()));
    989                     }
    990                 }
    991                 break;
    992 
    993             case SAVE_AS_PLAYLIST:
    994                 if (resultCode == RESULT_OK) {
    995                     Uri uri = intent.getData();
    996                     if (uri != null) {
    997                         long [] list = MusicUtils.getSongListForCursor(mTrackCursor);
    998                         int plid = Integer.parseInt(uri.getLastPathSegment());
    999                         MusicUtils.addToPlaylist(this, list, plid);
   1000                     }
   1001                 }
   1002                 break;
   1003         }
   1004     }
   1005 
   1006     private Cursor getTrackCursor(TrackListAdapter.TrackQueryHandler queryhandler, String filter,
   1007             boolean async) {
   1008 
   1009         if (queryhandler == null) {
   1010             throw new IllegalArgumentException();
   1011         }
   1012 
   1013         Cursor ret = null;
   1014         mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
   1015         StringBuilder where = new StringBuilder();
   1016         where.append(MediaStore.Audio.Media.TITLE + " != ''");
   1017 
   1018         if (mGenre != null) {
   1019             Uri uri = MediaStore.Audio.Genres.Members.getContentUri("external",
   1020                     Integer.valueOf(mGenre));
   1021             if (!TextUtils.isEmpty(filter)) {
   1022                 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build();
   1023             }
   1024             mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER;
   1025             ret = queryhandler.doQuery(uri,
   1026                     mCursorCols, where.toString(), null, mSortOrder, async);
   1027         } else if (mPlaylist != null) {
   1028             if (mPlaylist.equals("nowplaying")) {
   1029                 if (MusicUtils.sService != null) {
   1030                     ret = new NowPlayingCursor(MusicUtils.sService, mCursorCols);
   1031                     if (ret.getCount() == 0) {
   1032                         finish();
   1033                     }
   1034                 } else {
   1035                     // Nothing is playing.
   1036                 }
   1037             } else if (mPlaylist.equals("podcasts")) {
   1038                 where.append(" AND " + MediaStore.Audio.Media.IS_PODCAST + "=1");
   1039                 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
   1040                 if (!TextUtils.isEmpty(filter)) {
   1041                     uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build();
   1042                 }
   1043                 ret = queryhandler.doQuery(uri,
   1044                         mCursorCols, where.toString(), null,
   1045                         MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async);
   1046             } else if (mPlaylist.equals("recentlyadded")) {
   1047                 // do a query for all songs added in the last X weeks
   1048                 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
   1049                 if (!TextUtils.isEmpty(filter)) {
   1050                     uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build();
   1051                 }
   1052                 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7);
   1053                 where.append(" AND " + MediaStore.MediaColumns.DATE_ADDED + ">");
   1054                 where.append(System.currentTimeMillis() / 1000 - X);
   1055                 ret = queryhandler.doQuery(uri,
   1056                         mCursorCols, where.toString(), null,
   1057                         MediaStore.Audio.Media.DEFAULT_SORT_ORDER, async);
   1058             } else {
   1059                 Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external",
   1060                         Long.valueOf(mPlaylist));
   1061                 if (!TextUtils.isEmpty(filter)) {
   1062                     uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build();
   1063                 }
   1064                 mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER;
   1065                 ret = queryhandler.doQuery(uri, mPlaylistMemberCols,
   1066                         where.toString(), null, mSortOrder, async);
   1067             }
   1068         } else {
   1069             if (mAlbumId != null) {
   1070                 where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "=" + mAlbumId);
   1071                 mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder;
   1072             }
   1073             if (mArtistId != null) {
   1074                 where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId);
   1075             }
   1076             where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
   1077             Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
   1078             if (!TextUtils.isEmpty(filter)) {
   1079                 uri = uri.buildUpon().appendQueryParameter("filter", Uri.encode(filter)).build();
   1080             }
   1081             ret = queryhandler.doQuery(uri,
   1082                     mCursorCols, where.toString() , null, mSortOrder, async);
   1083         }
   1084 
   1085         // This special case is for the "nowplaying" cursor, which cannot be handled
   1086         // asynchronously using AsyncQueryHandler, so we do some extra initialization here.
   1087         if (ret != null && async) {
   1088             init(ret, false);
   1089             setTitle();
   1090         }
   1091         return ret;
   1092     }
   1093 
   1094     private class NowPlayingCursor extends AbstractCursor
   1095     {
   1096         public NowPlayingCursor(IMediaPlaybackService service, String [] cols)
   1097         {
   1098             mCols = cols;
   1099             mService  = service;
   1100             makeNowPlayingCursor();
   1101         }
   1102         private void makeNowPlayingCursor() {
   1103             mCurrentPlaylistCursor = null;
   1104             try {
   1105                 mNowPlaying = mService.getQueue();
   1106             } catch (RemoteException ex) {
   1107                 mNowPlaying = new long[0];
   1108             }
   1109             mSize = mNowPlaying.length;
   1110             if (mSize == 0) {
   1111                 return;
   1112             }
   1113 
   1114             StringBuilder where = new StringBuilder();
   1115             where.append(MediaStore.Audio.Media._ID + " IN (");
   1116             for (int i = 0; i < mSize; i++) {
   1117                 where.append(mNowPlaying[i]);
   1118                 if (i < mSize - 1) {
   1119                     where.append(",");
   1120                 }
   1121             }
   1122             where.append(")");
   1123 
   1124             mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this,
   1125                     MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
   1126                     mCols, where.toString(), null, MediaStore.Audio.Media._ID);
   1127 
   1128             if (mCurrentPlaylistCursor == null) {
   1129                 mSize = 0;
   1130                 return;
   1131             }
   1132 
   1133             int size = mCurrentPlaylistCursor.getCount();
   1134             mCursorIdxs = new long[size];
   1135             mCurrentPlaylistCursor.moveToFirst();
   1136             int colidx = mCurrentPlaylistCursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
   1137             for (int i = 0; i < size; i++) {
   1138                 mCursorIdxs[i] = mCurrentPlaylistCursor.getLong(colidx);
   1139                 mCurrentPlaylistCursor.moveToNext();
   1140             }
   1141             mCurrentPlaylistCursor.moveToFirst();
   1142             mCurPos = -1;
   1143 
   1144             // At this point we can verify the 'now playing' list we got
   1145             // earlier to make sure that all the items in there still exist
   1146             // in the database, and remove those that aren't. This way we
   1147             // don't get any blank items in the list.
   1148             try {
   1149                 int removed = 0;
   1150                 for (int i = mNowPlaying.length - 1; i >= 0; i--) {
   1151                     long trackid = mNowPlaying[i];
   1152                     int crsridx = Arrays.binarySearch(mCursorIdxs, trackid);
   1153                     if (crsridx < 0) {
   1154                         //Log.i("@@@@@", "item no longer exists in db: " + trackid);
   1155                         removed += mService.removeTrack(trackid);
   1156                     }
   1157                 }
   1158                 if (removed > 0) {
   1159                     mNowPlaying = mService.getQueue();
   1160                     mSize = mNowPlaying.length;
   1161                     if (mSize == 0) {
   1162                         mCursorIdxs = null;
   1163                         return;
   1164                     }
   1165                 }
   1166             } catch (RemoteException ex) {
   1167                 mNowPlaying = new long[0];
   1168             }
   1169         }
   1170 
   1171         @Override
   1172         public int getCount()
   1173         {
   1174             return mSize;
   1175         }
   1176 
   1177         @Override
   1178         public boolean onMove(int oldPosition, int newPosition)
   1179         {
   1180             if (oldPosition == newPosition)
   1181                 return true;
   1182 
   1183             if (mNowPlaying == null || mCursorIdxs == null || newPosition >= mNowPlaying.length) {
   1184                 return false;
   1185             }
   1186 
   1187             // The cursor doesn't have any duplicates in it, and is not ordered
   1188             // in queue-order, so we need to figure out where in the cursor we
   1189             // should be.
   1190 
   1191             long newid = mNowPlaying[newPosition];
   1192             int crsridx = Arrays.binarySearch(mCursorIdxs, newid);
   1193             mCurrentPlaylistCursor.moveToPosition(crsridx);
   1194             mCurPos = newPosition;
   1195 
   1196             return true;
   1197         }
   1198 
   1199         public boolean removeItem(int which)
   1200         {
   1201             try {
   1202                 if (mService.removeTracks(which, which) == 0) {
   1203                     return false; // delete failed
   1204                 }
   1205                 int i = (int) which;
   1206                 mSize--;
   1207                 while (i < mSize) {
   1208                     mNowPlaying[i] = mNowPlaying[i+1];
   1209                     i++;
   1210                 }
   1211                 onMove(-1, (int) mCurPos);
   1212             } catch (RemoteException ex) {
   1213             }
   1214             return true;
   1215         }
   1216 
   1217         public void moveItem(int from, int to) {
   1218             try {
   1219                 mService.moveQueueItem(from, to);
   1220                 mNowPlaying = mService.getQueue();
   1221                 onMove(-1, mCurPos); // update the underlying cursor
   1222             } catch (RemoteException ex) {
   1223             }
   1224         }
   1225 
   1226         private void dump() {
   1227             String where = "(";
   1228             for (int i = 0; i < mSize; i++) {
   1229                 where += mNowPlaying[i];
   1230                 if (i < mSize - 1) {
   1231                     where += ",";
   1232                 }
   1233             }
   1234             where += ")";
   1235             Log.i("NowPlayingCursor: ", where);
   1236         }
   1237 
   1238         @Override
   1239         public String getString(int column)
   1240         {
   1241             try {
   1242                 return mCurrentPlaylistCursor.getString(column);
   1243             } catch (Exception ex) {
   1244                 onChange(true);
   1245                 return "";
   1246             }
   1247         }
   1248 
   1249         @Override
   1250         public short getShort(int column)
   1251         {
   1252             return mCurrentPlaylistCursor.getShort(column);
   1253         }
   1254 
   1255         @Override
   1256         public int getInt(int column)
   1257         {
   1258             try {
   1259                 return mCurrentPlaylistCursor.getInt(column);
   1260             } catch (Exception ex) {
   1261                 onChange(true);
   1262                 return 0;
   1263             }
   1264         }
   1265 
   1266         @Override
   1267         public long getLong(int column)
   1268         {
   1269             try {
   1270                 return mCurrentPlaylistCursor.getLong(column);
   1271             } catch (Exception ex) {
   1272                 onChange(true);
   1273                 return 0;
   1274             }
   1275         }
   1276 
   1277         @Override
   1278         public float getFloat(int column)
   1279         {
   1280             return mCurrentPlaylistCursor.getFloat(column);
   1281         }
   1282 
   1283         @Override
   1284         public double getDouble(int column)
   1285         {
   1286             return mCurrentPlaylistCursor.getDouble(column);
   1287         }
   1288 
   1289         @Override
   1290         public boolean isNull(int column)
   1291         {
   1292             return mCurrentPlaylistCursor.isNull(column);
   1293         }
   1294 
   1295         @Override
   1296         public String[] getColumnNames()
   1297         {
   1298             return mCols;
   1299         }
   1300 
   1301         @Override
   1302         public void deactivate()
   1303         {
   1304             if (mCurrentPlaylistCursor != null)
   1305                 mCurrentPlaylistCursor.deactivate();
   1306         }
   1307 
   1308         @Override
   1309         public boolean requery()
   1310         {
   1311             makeNowPlayingCursor();
   1312             return true;
   1313         }
   1314 
   1315         private String [] mCols;
   1316         private Cursor mCurrentPlaylistCursor;     // updated in onMove
   1317         private int mSize;          // size of the queue
   1318         private long[] mNowPlaying;
   1319         private long[] mCursorIdxs;
   1320         private int mCurPos;
   1321         private IMediaPlaybackService mService;
   1322     }
   1323 
   1324     static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer {
   1325         boolean mIsNowPlaying;
   1326         boolean mDisableNowPlayingIndicator;
   1327 
   1328         int mTitleIdx;
   1329         int mArtistIdx;
   1330         int mDurationIdx;
   1331         int mAudioIdIdx;
   1332 
   1333         private final StringBuilder mBuilder = new StringBuilder();
   1334         private final String mUnknownArtist;
   1335         private final String mUnknownAlbum;
   1336 
   1337         private AlphabetIndexer mIndexer;
   1338 
   1339         private TrackBrowserActivity mActivity = null;
   1340         private TrackQueryHandler mQueryHandler;
   1341         private String mConstraint = null;
   1342         private boolean mConstraintIsValid = false;
   1343 
   1344         static class ViewHolder {
   1345             TextView line1;
   1346             TextView line2;
   1347             TextView duration;
   1348             ImageView play_indicator;
   1349             CharArrayBuffer buffer1;
   1350             char [] buffer2;
   1351         }
   1352 
   1353         class TrackQueryHandler extends AsyncQueryHandler {
   1354 
   1355             class QueryArgs {
   1356                 public Uri uri;
   1357                 public String [] projection;
   1358                 public String selection;
   1359                 public String [] selectionArgs;
   1360                 public String orderBy;
   1361             }
   1362 
   1363             TrackQueryHandler(ContentResolver res) {
   1364                 super(res);
   1365             }
   1366 
   1367             public Cursor doQuery(Uri uri, String[] projection,
   1368                     String selection, String[] selectionArgs,
   1369                     String orderBy, boolean async) {
   1370                 if (async) {
   1371                     // Get 100 results first, which is enough to allow the user to start scrolling,
   1372                     // while still being very fast.
   1373                     Uri limituri = uri.buildUpon().appendQueryParameter("limit", "100").build();
   1374                     QueryArgs args = new QueryArgs();
   1375                     args.uri = uri;
   1376                     args.projection = projection;
   1377                     args.selection = selection;
   1378                     args.selectionArgs = selectionArgs;
   1379                     args.orderBy = orderBy;
   1380 
   1381                     startQuery(0, args, limituri, projection, selection, selectionArgs, orderBy);
   1382                     return null;
   1383                 }
   1384                 return MusicUtils.query(mActivity,
   1385                         uri, projection, selection, selectionArgs, orderBy);
   1386             }
   1387 
   1388             @Override
   1389             protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
   1390                 //Log.i("@@@", "query complete: " + cursor.getCount() + "   " + mActivity);
   1391                 mActivity.init(cursor, cookie != null);
   1392                 if (token == 0 && cookie != null && cursor != null && cursor.getCount() >= 100) {
   1393                     QueryArgs args = (QueryArgs) cookie;
   1394                     startQuery(1, null, args.uri, args.projection, args.selection,
   1395                             args.selectionArgs, args.orderBy);
   1396                 }
   1397             }
   1398         }
   1399 
   1400         TrackListAdapter(Context context, TrackBrowserActivity currentactivity,
   1401                 int layout, Cursor cursor, String[] from, int[] to,
   1402                 boolean isnowplaying, boolean disablenowplayingindicator) {
   1403             super(context, layout, cursor, from, to);
   1404             mActivity = currentactivity;
   1405             getColumnIndices(cursor);
   1406             mIsNowPlaying = isnowplaying;
   1407             mDisableNowPlayingIndicator = disablenowplayingindicator;
   1408             mUnknownArtist = context.getString(R.string.unknown_artist_name);
   1409             mUnknownAlbum = context.getString(R.string.unknown_album_name);
   1410 
   1411             mQueryHandler = new TrackQueryHandler(context.getContentResolver());
   1412         }
   1413 
   1414         public void setActivity(TrackBrowserActivity newactivity) {
   1415             mActivity = newactivity;
   1416         }
   1417 
   1418         public TrackQueryHandler getQueryHandler() {
   1419             return mQueryHandler;
   1420         }
   1421 
   1422         private void getColumnIndices(Cursor cursor) {
   1423             if (cursor != null) {
   1424                 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
   1425                 mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST);
   1426                 mDurationIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION);
   1427                 try {
   1428                     mAudioIdIdx = cursor.getColumnIndexOrThrow(
   1429                             MediaStore.Audio.Playlists.Members.AUDIO_ID);
   1430                 } catch (IllegalArgumentException ex) {
   1431                     mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
   1432                 }
   1433 
   1434                 if (mIndexer != null) {
   1435                     mIndexer.setCursor(cursor);
   1436                 } else if (!mActivity.mEditMode && mActivity.mAlbumId == null) {
   1437                     String alpha = mActivity.getString(R.string.fast_scroll_alphabet);
   1438 
   1439                     mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha);
   1440                 }
   1441             }
   1442         }
   1443 
   1444         @Override
   1445         public View newView(Context context, Cursor cursor, ViewGroup parent) {
   1446             View v = super.newView(context, cursor, parent);
   1447             ImageView iv = (ImageView) v.findViewById(R.id.icon);
   1448             iv.setVisibility(View.GONE);
   1449 
   1450             ViewHolder vh = new ViewHolder();
   1451             vh.line1 = (TextView) v.findViewById(R.id.line1);
   1452             vh.line2 = (TextView) v.findViewById(R.id.line2);
   1453             vh.duration = (TextView) v.findViewById(R.id.duration);
   1454             vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
   1455             vh.buffer1 = new CharArrayBuffer(100);
   1456             vh.buffer2 = new char[200];
   1457             v.setTag(vh);
   1458             return v;
   1459         }
   1460 
   1461         @Override
   1462         public void bindView(View view, Context context, Cursor cursor) {
   1463 
   1464             ViewHolder vh = (ViewHolder) view.getTag();
   1465 
   1466             cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
   1467             vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
   1468 
   1469             int secs = cursor.getInt(mDurationIdx) / 1000;
   1470             if (secs == 0) {
   1471                 vh.duration.setText("");
   1472             } else {
   1473                 vh.duration.setText(MusicUtils.makeTimeString(context, secs));
   1474             }
   1475 
   1476             final StringBuilder builder = mBuilder;
   1477             builder.delete(0, builder.length());
   1478 
   1479             String name = cursor.getString(mArtistIdx);
   1480             if (name == null || name.equals(MediaStore.UNKNOWN_STRING)) {
   1481                 builder.append(mUnknownArtist);
   1482             } else {
   1483                 builder.append(name);
   1484             }
   1485             int len = builder.length();
   1486             if (vh.buffer2.length < len) {
   1487                 vh.buffer2 = new char[len];
   1488             }
   1489             builder.getChars(0, len, vh.buffer2, 0);
   1490             vh.line2.setText(vh.buffer2, 0, len);
   1491 
   1492             ImageView iv = vh.play_indicator;
   1493             long id = -1;
   1494             if (MusicUtils.sService != null) {
   1495                 // TODO: IPC call on each bind??
   1496                 try {
   1497                     if (mIsNowPlaying) {
   1498                         id = MusicUtils.sService.getQueuePosition();
   1499                     } else {
   1500                         id = MusicUtils.sService.getAudioId();
   1501                     }
   1502                 } catch (RemoteException ex) {
   1503                 }
   1504             }
   1505 
   1506             // Determining whether and where to show the "now playing indicator
   1507             // is tricky, because we don't actually keep track of where the songs
   1508             // in the current playlist came from after they've started playing.
   1509             //
   1510             // If the "current playlists" is shown, then we can simply match by position,
   1511             // otherwise, we need to match by id. Match-by-id gets a little weird if
   1512             // a song appears in a playlist more than once, and you're in edit-playlist
   1513             // mode. In that case, both items will have the "now playing" indicator.
   1514             // For this reason, we don't show the play indicator at all when in edit
   1515             // playlist mode (except when you're viewing the "current playlist",
   1516             // which is not really a playlist)
   1517             if ( (mIsNowPlaying && cursor.getPosition() == id) ||
   1518                  (!mIsNowPlaying && !mDisableNowPlayingIndicator && cursor.getLong(mAudioIdIdx) == id)) {
   1519                 iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
   1520                 iv.setVisibility(View.VISIBLE);
   1521             } else {
   1522                 iv.setVisibility(View.GONE);
   1523             }
   1524         }
   1525 
   1526         @Override
   1527         public void changeCursor(Cursor cursor) {
   1528             if (mActivity.isFinishing() && cursor != null) {
   1529                 cursor.close();
   1530                 cursor = null;
   1531             }
   1532             if (cursor != mActivity.mTrackCursor) {
   1533                 mActivity.mTrackCursor = cursor;
   1534                 super.changeCursor(cursor);
   1535                 getColumnIndices(cursor);
   1536             }
   1537         }
   1538 
   1539         @Override
   1540         public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
   1541             String s = constraint.toString();
   1542             if (mConstraintIsValid && (
   1543                     (s == null && mConstraint == null) ||
   1544                     (s != null && s.equals(mConstraint)))) {
   1545                 return getCursor();
   1546             }
   1547             Cursor c = mActivity.getTrackCursor(mQueryHandler, s, false);
   1548             mConstraint = s;
   1549             mConstraintIsValid = true;
   1550             return c;
   1551         }
   1552 
   1553         // SectionIndexer methods
   1554 
   1555         public Object[] getSections() {
   1556             if (mIndexer != null) {
   1557                 return mIndexer.getSections();
   1558             } else {
   1559                 return null;
   1560             }
   1561         }
   1562 
   1563         public int getPositionForSection(int section) {
   1564             int pos = mIndexer.getPositionForSection(section);
   1565             return pos;
   1566         }
   1567 
   1568         public int getSectionForPosition(int position) {
   1569             return 0;
   1570         }
   1571     }
   1572 }
   1573 
   1574