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