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