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