OSDN Git Service

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