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