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