OSDN Git Service

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