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