1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.music;
18
19import com.android.music.MusicUtils.ServiceToken;
20
21import android.app.ListActivity;
22import android.app.SearchManager;
23import android.content.AsyncQueryHandler;
24import android.content.BroadcastReceiver;
25import android.content.ComponentName;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.content.ServiceConnection;
33import android.database.AbstractCursor;
34import android.database.CharArrayBuffer;
35import android.database.Cursor;
36import android.graphics.Bitmap;
37import android.media.AudioManager;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.Handler;
41import android.os.IBinder;
42import android.os.Message;
43import android.os.RemoteException;
44import android.provider.MediaStore;
45import android.provider.MediaStore.Audio.Playlists;
46import android.text.TextUtils;
47import android.util.Log;
48import android.view.ContextMenu;
49import android.view.KeyEvent;
50import android.view.Menu;
51import android.view.MenuItem;
52import android.view.SubMenu;
53import android.view.View;
54import android.view.ViewGroup;
55import android.view.Window;
56import android.view.ContextMenu.ContextMenuInfo;
57import android.widget.AlphabetIndexer;
58import android.widget.ImageView;
59import android.widget.ListView;
60import android.widget.SectionIndexer;
61import android.widget.SimpleCursorAdapter;
62import android.widget.TextView;
63import android.widget.AdapterView.AdapterContextMenuInfo;
64
65import java.text.Collator;
66import java.util.Arrays;
67
68public 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