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