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