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