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