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