2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.music;
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;
59 import java.text.Collator;
60 import java.util.Arrays;
62 public class TrackBrowserActivity extends ListActivity
63 implements View.OnCreateContextMenuListener, MusicUtils.Defs
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;
74 private static final String LOGTAG = "TrackBrowser";
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;
95 public TrackBrowserActivity()
99 /** Called when the activity is first created. */
101 public void onCreate(Bundle icicle)
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);
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);
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
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
147 MusicUtils.bindToService(this);
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);
156 setContentView(R.layout.media_picker_activity);
157 mTrackList = getListView();
158 mTrackList.setOnCreateContextMenuListener(this);
160 //((TouchInterceptor) mTrackList).setDragListener(mDragListener);
161 ((TouchInterceptor) mTrackList).setDropListener(mDropListener);
162 ((TouchInterceptor) mTrackList).setRemoveListener(mRemoveListener);
163 mTrackList.setCacheColorHint(0);
165 mTrackList.setTextFilterEnabled(true);
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
173 mEditMode ? R.layout.edit_track_list_item : R.layout.track_list_item,
177 "nowplaying".equals(mPlaylist),
179 setListAdapter(mAdapter);
180 setTitle(R.string.working_songs);
181 getTrackCursor(mAdapter.getQueryHandler(), null);
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) {
195 setTitle(R.string.working_songs);
196 getTrackCursor(mAdapter.getQueryHandler(), null);
202 public Object onRetainNonConfigurationInstance() {
203 TrackListAdapter a = mAdapter;
209 public void onDestroy() {
210 MusicUtils.unbindFromService(this);
212 if ("nowplaying".equals(mPlaylist)) {
213 unregisterReceiver(mNowPlayingListener);
215 unregisterReceiver(mTrackListListener);
217 } catch (IllegalArgumentException ex) {
218 // we end up here in case we never registered the listeners
221 // if we didn't send the adapter off to another activity, we should
224 Cursor c = mAdapter.getCursor();
229 unregisterReceiver(mScanListener);
234 public void onResume() {
236 if (mTrackCursor != null) {
237 getListView().invalidateViews();
239 MusicUtils.setSpinnerState(this);
242 public void onPause() {
243 mReScanHandler.removeCallbacksAndMessages(null);
248 * This listener gets called when the media scanner starts up or finishes, and
249 * when the sd card is unmounted.
251 private BroadcastReceiver mScanListener = new BroadcastReceiver() {
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);
259 mReScanHandler.sendEmptyMessage(0);
263 private Handler mReScanHandler = new Handler() {
265 public void handleMessage(Message msg) {
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.
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);
287 public void init(Cursor newCursor) {
289 mAdapter.changeCursor(newCursor); // also sets mTrackCursor
291 if (mTrackCursor == null) {
292 MusicUtils.displayDatabaseError(this);
294 mReScanHandler.sendEmptyMessageDelayed(0, 1000);
298 MusicUtils.hideDatabaseError(this);
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)) {
308 int cur = MusicUtils.sService.getQueuePosition();
310 registerReceiver(mNowPlayingListener, new IntentFilter(f));
311 mNowPlayingListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED));
312 } catch (RemoteException ex) {
315 String key = getIntent().getStringExtra("artist");
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());
325 mTrackCursor.moveToNext();
328 registerReceiver(mTrackListListener, new IntentFilter(f));
329 mTrackListListener.onReceive(this, new Intent(MediaPlaybackService.META_CHANGED));
333 private void setTitle() {
334 int numresults = mTrackCursor != null ? mTrackCursor.getCount() : 0;
335 if (numresults > 0) {
336 mTrackCursor.moveToFirst();
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) {
357 fancyName = mTrackCursor.getString(idx);
361 if (fancyName.equals(MediaFile.UNKNOWN_STRING)) {
362 fancyName = getString(R.string.unknown_album_name);
364 } else if (mPlaylist != null) {
365 if (mPlaylist.equals("nowplaying")) {
366 if (MusicUtils.getCurrentShuffleMode() == MediaPlaybackService.SHUFFLE_AUTO) {
367 fancyName = getText(R.string.partyshuffle_title);
369 fancyName = getText(R.string.nowplaying_title);
372 String [] cols = new String [] {
373 MediaStore.Audio.Playlists.NAME
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);
386 } else if (mGenre != null) {
387 String [] cols = new String [] {
388 MediaStore.Audio.Genres.NAME
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);
402 if (fancyName != null) {
405 setTitle(R.string.tracks_title);
408 setTitle(R.string.no_tracks_title);
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;
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;
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();
443 int colidx = mTrackCursor.getColumnIndexOrThrow(
444 MediaStore.Audio.Playlists.Members.PLAY_ORDER);
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);
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);
478 private TouchInterceptor.RemoveListener mRemoveListener =
479 new TouchInterceptor.RemoveListener() {
480 public void remove(int which) {
481 removePlaylistItem(which);
485 private void removePlaylistItem(int which) {
486 View v = mTrackList.getChildAt(which - mTrackList.getFirstVisiblePosition());
488 if (MusicUtils.sService != null
489 && which != MusicUtils.sService.getQueuePosition()) {
490 mDeletedOneRow = true;
492 } catch (RemoteException e) {
493 // Service died, so nothing playing.
494 mDeletedOneRow = true;
496 v.setVisibility(View.GONE);
497 mTrackList.invalidateViews();
498 if (mTrackCursor instanceof NowPlayingCursor) {
499 ((NowPlayingCursor)mTrackCursor).removeItem(which);
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);
510 v.setVisibility(View.VISIBLE);
511 mTrackList.invalidateViews();
514 private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
516 public void onReceive(Context context, Intent intent) {
517 getListView().invalidateViews();
521 private BroadcastReceiver mNowPlayingListener = new BroadcastReceiver() {
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
531 mDeletedOneRow = false;
534 Cursor c = new NowPlayingCursor(MusicUtils.sService, mCursorCols);
535 if (c.getCount() == 0) {
539 mAdapter.changeCursor(c);
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);
550 menu.add(0, REMOVE, 0, R.string.remove_from_playlist);
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);
559 int id_idx = mTrackCursor.getColumnIndexOrThrow(
560 MediaStore.Audio.Playlists.Members.AUDIO_ID);
561 mSelectedId = mTrackCursor.getInt(id_idx);
562 } catch (IllegalArgumentException ex) {
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);
575 public boolean onContextItemSelected(MenuItem item) {
576 switch (item.getItemId()) {
577 case PLAY_SELECTION: {
579 int position = mSelectedPosition;
580 MusicUtils.playAll(this, mTrackCursor, position);
585 int [] list = new int[] { (int) mSelectedId };
586 MusicUtils.addToCurrentPlaylist(this, list);
591 Intent intent = new Intent();
592 intent.setClass(this, CreatePlaylist.class);
593 startActivityForResult(intent, NEW_PLAYLIST);
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);
604 case USE_AS_RINGTONE:
605 // Set the system setting to make this the current ringtone
606 MusicUtils.setRingtone(this, mSelectedId);
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);
620 startActivityForResult(intent, -1);
625 removePlaylistItem(mSelectedPosition);
632 return super.onContextItemSelected(item);
636 CharSequence title = null;
639 Intent i = new Intent();
640 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
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);
650 startActivity(Intent.createChooser(i, title));
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)
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:
664 case KeyEvent.KEYCODE_DPAD_DOWN:
667 case KeyEvent.KEYCODE_DEL:
673 return super.dispatchKeyEvent(event);
676 private void removeItem() {
677 int curcount = mTrackCursor.getCount();
678 int curpos = mTrackList.getSelectedItemPosition();
679 if (curcount == 0 || curpos < 0) {
683 if ("nowplaying".equals(mPlaylist)) {
684 // remove track from queue
686 // Work around bug 902971. To get quick visual feedback
687 // of the deletion of the item, hide the selected view.
689 if (curpos != MusicUtils.sService.getQueuePosition()) {
690 mDeletedOneRow = true;
692 } catch (RemoteException ex) {
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();
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);
714 mTrackList.setSelection(curpos < curcount ? curpos : curcount);
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)) {
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;
733 mTrackList.setSelection(curpos - 1);
735 mTrackList.setSelection(curpos + 1);
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();
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();
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();
759 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, currentplayidx);
760 wherearg[0] = mTrackCursor.getString(0);
761 res.update(baseUri, values, where, wherearg);
766 protected void onListItemClick(ListView l, View v, int position, long id)
768 if (mTrackCursor.getCount() == 0) {
771 MusicUtils.playAll(this, mTrackCursor, position);
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
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);
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);
799 public boolean onOptionsItemSelected(MenuItem item) {
802 switch (item.getItemId()) {
804 MusicUtils.playAll(this, mTrackCursor);
809 intent = new Intent();
810 intent.setClass(this, MusicBrowserActivity.class);
811 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
812 startActivity(intent);
816 intent = new Intent("com.android.music.PLAYBACK_VIEWER");
817 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
818 startActivity(intent);
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);
833 case SAVE_AS_PLAYLIST:
834 intent = new Intent();
835 intent.setClass(this, CreatePlaylist.class);
836 startActivityForResult(intent, SAVE_AS_PLAYLIST);
840 // We only clear the current playlist
841 MusicUtils.clearQueue();
844 return super.onOptionsItemSelected(item);
848 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
849 switch (requestCode) {
851 if (resultCode == RESULT_CANCELED) {
854 getTrackCursor(mAdapter.getQueryHandler(), null);
859 if (resultCode == RESULT_OK) {
860 Uri uri = intent.getData();
862 int [] list = new int[] { (int) mSelectedId };
863 MusicUtils.addToPlaylist(this, list, Integer.valueOf(uri.getLastPathSegment()));
868 case SAVE_AS_PLAYLIST:
869 if (resultCode == RESULT_OK) {
870 Uri uri = intent.getData();
872 int [] list = MusicUtils.getSongListForCursor(mTrackCursor);
873 int plid = Integer.parseInt(uri.getLastPathSegment());
874 MusicUtils.addToPlaylist(this, list, plid);
881 private Cursor getTrackCursor(AsyncQueryHandler async, String filter) {
883 mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
884 StringBuilder where = new StringBuilder();
885 where.append(MediaStore.Audio.Media.TITLE + " != ''");
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]) + '%';
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 ?");
905 if (mGenre != null) {
906 mSortOrder = MediaStore.Audio.Genres.Members.DEFAULT_SORT_ORDER;
908 async.startQuery(0, null,
909 MediaStore.Audio.Genres.Members.getContentUri("external",
910 Integer.valueOf(mGenre)),
911 mCursorCols, where.toString(), keywords, mSortOrder);
914 ret = MusicUtils.query(this,
915 MediaStore.Audio.Genres.Members.getContentUri("external", Integer.valueOf(mGenre)),
916 mCursorCols, where.toString(), keywords, mSortOrder);
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) {
926 // Nothing is playing.
929 mSortOrder = MediaStore.Audio.Playlists.Members.DEFAULT_SORT_ORDER;
931 async.startQuery(0, null,
932 MediaStore.Audio.Playlists.Members.getContentUri("external", Long.valueOf(mPlaylist)),
933 mPlaylistMemberCols, where.toString(), keywords, mSortOrder);
936 ret = MusicUtils.query(this,
937 MediaStore.Audio.Playlists.Members.getContentUri("external", Long.valueOf(mPlaylist)),
938 mPlaylistMemberCols, where.toString(), keywords, mSortOrder);
942 if (mAlbumId != null) {
943 where.append(" AND " + MediaStore.Audio.Media.ALBUM_ID + "=" + mAlbumId);
944 mSortOrder = MediaStore.Audio.Media.TRACK + ", " + mSortOrder;
946 if (mArtistId != null) {
947 where.append(" AND " + MediaStore.Audio.Media.ARTIST_ID + "=" + mArtistId);
949 where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
951 async.startQuery(0, null,
952 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
953 mCursorCols, where.toString() , keywords, mSortOrder);
956 ret = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
957 mCursorCols, where.toString() , keywords, mSortOrder);
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) {
970 private class NowPlayingCursor extends AbstractCursor
972 public NowPlayingCursor(IMediaPlaybackService service, String [] cols)
976 makeNowPlayingCursor();
978 private void makeNowPlayingCursor() {
979 mCurrentPlaylistCursor = null;
981 mNowPlaying = mService.getQueue();
982 } catch (RemoteException ex) {
983 mNowPlaying = new int[0];
985 mSize = mNowPlaying.length;
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]);
1000 mCurrentPlaylistCursor = MusicUtils.query(TrackBrowserActivity.this,
1001 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
1002 mCols, where.toString(), null, MediaStore.Audio.Media._ID);
1004 if (mCurrentPlaylistCursor == null) {
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();
1017 mCurrentPlaylistCursor.moveToFirst();
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.
1026 for (int i = mNowPlaying.length - 1; i >= 0; i--) {
1027 int trackid = mNowPlaying[i];
1028 int crsridx = Arrays.binarySearch(mCursorIdxs, trackid);
1030 //Log.i("@@@@@", "item no longer exists in db: " + trackid);
1031 removed += mService.removeTrack(trackid);
1035 mNowPlaying = mService.getQueue();
1036 mSize = mNowPlaying.length;
1042 } catch (RemoteException ex) {
1043 mNowPlaying = new int[0];
1048 public int getCount()
1054 public boolean onMove(int oldPosition, int newPosition)
1056 if (oldPosition == newPosition)
1059 if (mNowPlaying == null || mCursorIdxs == null) {
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
1067 int newid = mNowPlaying[newPosition];
1068 int crsridx = Arrays.binarySearch(mCursorIdxs, newid);
1069 mCurrentPlaylistCursor.moveToPosition(crsridx);
1070 mCurPos = newPosition;
1075 public boolean removeItem(int which)
1078 if (mService.removeTracks(which, which) == 0) {
1079 return false; // delete failed
1081 int i = (int) which;
1084 mNowPlaying[i] = mNowPlaying[i+1];
1087 onMove(-1, (int) mCurPos);
1088 } catch (RemoteException ex) {
1093 public void moveItem(int from, int to) {
1095 mService.moveQueueItem(from, to);
1096 mNowPlaying = mService.getQueue();
1097 onMove(-1, mCurPos); // update the underlying cursor
1098 } catch (RemoteException ex) {
1102 private void dump() {
1104 for (int i = 0; i < mSize; i++) {
1105 where += mNowPlaying[i];
1106 if (i < mSize - 1) {
1111 Log.i("NowPlayingCursor: ", where);
1115 public String getString(int column)
1118 return mCurrentPlaylistCursor.getString(column);
1119 } catch (Exception ex) {
1126 public short getShort(int column)
1128 return mCurrentPlaylistCursor.getShort(column);
1132 public int getInt(int column)
1135 return mCurrentPlaylistCursor.getInt(column);
1136 } catch (Exception ex) {
1143 public long getLong(int column)
1146 return mCurrentPlaylistCursor.getLong(column);
1147 } catch (Exception ex) {
1154 public float getFloat(int column)
1156 return mCurrentPlaylistCursor.getFloat(column);
1160 public double getDouble(int column)
1162 return mCurrentPlaylistCursor.getDouble(column);
1166 public boolean isNull(int column)
1168 return mCurrentPlaylistCursor.isNull(column);
1172 public String[] getColumnNames()
1178 public void deactivate()
1180 if (mCurrentPlaylistCursor != null)
1181 mCurrentPlaylistCursor.deactivate();
1185 public boolean requery()
1187 makeNowPlayingCursor();
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;
1200 static class TrackListAdapter extends SimpleCursorAdapter implements SectionIndexer {
1201 boolean mIsNowPlaying;
1202 boolean mDisableNowPlayingIndicator;
1210 private final StringBuilder mBuilder = new StringBuilder();
1211 private final String mUnknownArtist;
1212 private final String mUnknownAlbum;
1214 private AlphabetIndexer mIndexer;
1216 private TrackBrowserActivity mActivity = null;
1217 private AsyncQueryHandler mQueryHandler;
1223 ImageView play_indicator;
1224 CharArrayBuffer buffer1;
1228 class QueryHandler extends AsyncQueryHandler {
1229 QueryHandler(ContentResolver res) {
1234 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
1235 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity);
1236 mActivity.init(cursor);
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);
1251 mQueryHandler = new QueryHandler(context.getContentResolver());
1254 public void setActivity(TrackBrowserActivity newactivity) {
1255 mActivity = newactivity;
1258 public AsyncQueryHandler getQueryHandler() {
1259 return mQueryHandler;
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);
1269 mAudioIdIdx = cursor.getColumnIndexOrThrow(
1270 MediaStore.Audio.Playlists.Members.AUDIO_ID);
1271 } catch (IllegalArgumentException ex) {
1272 mAudioIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
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);
1281 mIndexer = new MusicAlphabetIndexer(cursor, mTitleIdx, alpha);
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;
1297 iv.setVisibility(View.GONE);
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];
1312 public void bindView(View view, Context context, Cursor cursor) {
1314 ViewHolder vh = (ViewHolder) view.getTag();
1316 cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
1317 vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
1319 int secs = cursor.getInt(mDurationIdx) / 1000;
1321 vh.duration.setText("");
1323 vh.duration.setText(MusicUtils.makeTimeString(context, secs));
1326 final StringBuilder builder = mBuilder;
1327 builder.delete(0, builder.length());
1329 String name = cursor.getString(mAlbumIdx);
1330 if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
1331 builder.append(mUnknownAlbum);
1333 builder.append(name);
1335 builder.append('\n');
1336 name = cursor.getString(mArtistIdx);
1337 if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
1338 builder.append(mUnknownArtist);
1340 builder.append(name);
1342 int len = builder.length();
1343 if (vh.buffer2.length < len) {
1344 vh.buffer2 = new char[len];
1346 builder.getChars(0, len, vh.buffer2, 0);
1347 vh.line2.setText(vh.buffer2, 0, len);
1349 ImageView iv = vh.play_indicator;
1351 if (MusicUtils.sService != null) {
1352 // TODO: IPC call on each bind??
1354 if (mIsNowPlaying) {
1355 id = MusicUtils.sService.getQueuePosition();
1357 id = MusicUtils.sService.getAudioId();
1359 } catch (RemoteException ex) {
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.
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);
1379 iv.setVisibility(View.GONE);
1384 public void changeCursor(Cursor cursor) {
1385 if (cursor != mActivity.mTrackCursor) {
1386 mActivity.mTrackCursor = cursor;
1387 super.changeCursor(cursor);
1388 getColumnIndices(cursor);
1393 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
1394 return mActivity.getTrackCursor(null, constraint.toString());
1397 // SectionIndexer methods
1399 public Object[] getSections() {
1400 if (mIndexer != null) {
1401 return mIndexer.getSections();
1407 public int getPositionForSection(int section) {
1408 int pos = mIndexer.getPositionForSection(section);
1412 public int getSectionForPosition(int position) {