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