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