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