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