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