1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.music; 18 19 import com.android.music.MusicUtils.ServiceToken; 20 21 import android.app.ListActivity; 22 import android.content.AsyncQueryHandler; 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.ContentResolver; 26 import android.content.ContentUris; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.content.ServiceConnection; 31 import android.database.Cursor; 32 import android.database.MatrixCursor; 33 import android.database.MergeCursor; 34 import android.database.sqlite.SQLiteException; 35 import android.media.AudioManager; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.IBinder; 40 import android.os.Message; 41 import android.provider.MediaStore; 42 import android.util.Log; 43 import android.view.ContextMenu; 44 import android.view.Menu; 45 import android.view.MenuItem; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.view.Window; 49 import android.view.ContextMenu.ContextMenuInfo; 50 import android.widget.ImageView; 51 import android.widget.ListView; 52 import android.widget.SimpleCursorAdapter; 53 import android.widget.TextView; 54 import android.widget.Toast; 55 import android.widget.AdapterView.AdapterContextMenuInfo; 56 57 import java.text.Collator; 58 import java.util.ArrayList; 59 60 public class PlaylistBrowserActivity extends ListActivity 61 implements View.OnCreateContextMenuListener, MusicUtils.Defs 62 { 63 private static final String TAG = "PlaylistBrowserActivity"; 64 private static final int DELETE_PLAYLIST = CHILD_MENU_BASE + 1; 65 private static final int EDIT_PLAYLIST = CHILD_MENU_BASE + 2; 66 private static final int RENAME_PLAYLIST = CHILD_MENU_BASE + 3; 67 private static final int CHANGE_WEEKS = CHILD_MENU_BASE + 4; 68 private static final long RECENTLY_ADDED_PLAYLIST = -1; 69 private static final long ALL_SONGS_PLAYLIST = -2; 70 private static final long PODCASTS_PLAYLIST = -3; 71 private PlaylistListAdapter mAdapter; 72 boolean mAdapterSent; 73 private static int mLastListPosCourse = -1; 74 private static int mLastListPosFine = -1; 75 76 private boolean mCreateShortcut; 77 private ServiceToken mToken; 78 79 public PlaylistBrowserActivity() 80 { 81 } 82 83 /** Called when the activity is first created. */ 84 @Override 85 public void onCreate(Bundle icicle) 86 { 87 super.onCreate(icicle); 88 89 final Intent intent = getIntent(); 90 final String action = intent.getAction(); 91 if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { 92 mCreateShortcut = true; 93 } 94 95 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 96 requestWindowFeature(Window.FEATURE_NO_TITLE); 97 setVolumeControlStream(AudioManager.STREAM_MUSIC); 98 mToken = MusicUtils.bindToService(this, new ServiceConnection() { 99 public void onServiceConnected(ComponentName classname, IBinder obj) { 100 if (Intent.ACTION_VIEW.equals(action)) { 101 Bundle b = intent.getExtras(); 102 if (b == null) { 103 Log.w(TAG, "Unexpected:getExtras() returns null."); 104 } else { 105 try { 106 long id = Long.parseLong(b.getString("playlist")); 107 if (id == RECENTLY_ADDED_PLAYLIST) { 108 playRecentlyAdded(); 109 } else if (id == PODCASTS_PLAYLIST) { 110 playPodcasts(); 111 } else if (id == ALL_SONGS_PLAYLIST) { 112 long[] list = MusicUtils.getAllSongs(PlaylistBrowserActivity.this); 113 if (list != null) { 114 MusicUtils.playAll(PlaylistBrowserActivity.this, list, 0); 115 } 116 } else { 117 MusicUtils.playPlaylist(PlaylistBrowserActivity.this, id); 118 } 119 } catch (NumberFormatException e) { 120 Log.w(TAG, "Playlist id missing or broken"); 121 } 122 } 123 finish(); 124 return; 125 } 126 MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this); 127 } 128 129 public void onServiceDisconnected(ComponentName classname) { 130 } 131 132 }); 133 IntentFilter f = new IntentFilter(); 134 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); 135 f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); 136 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 137 f.addDataScheme("file"); 138 registerReceiver(mScanListener, f); 139 140 setContentView(R.layout.media_picker_activity); 141 MusicUtils.updateButtonBar(this, R.id.playlisttab); 142 ListView lv = getListView(); 143 lv.setOnCreateContextMenuListener(this); 144 lv.setTextFilterEnabled(true); 145 146 mAdapter = (PlaylistListAdapter) getLastNonConfigurationInstance(); 147 if (mAdapter == null) { 148 //Log.i("@@@", "starting query"); 149 mAdapter = new PlaylistListAdapter( 150 getApplication(), 151 this, 152 R.layout.track_list_item, 153 mPlaylistCursor, 154 new String[] { MediaStore.Audio.Playlists.NAME}, 155 new int[] { android.R.id.text1 }); 156 setListAdapter(mAdapter); 157 setTitle(R.string.working_playlists); 158 getPlaylistCursor(mAdapter.getQueryHandler(), null); 159 } else { 160 mAdapter.setActivity(this); 161 setListAdapter(mAdapter); 162 mPlaylistCursor = mAdapter.getCursor(); 163 // If mPlaylistCursor is null, this can be because it doesn't have 164 // a cursor yet (because the initial query that sets its cursor 165 // is still in progress), or because the query failed. 166 // In order to not flash the error dialog at the user for the 167 // first case, simply retry the query when the cursor is null. 168 // Worst case, we end up doing the same query twice. 169 if (mPlaylistCursor != null) { 170 init(mPlaylistCursor); 171 } else { 172 setTitle(R.string.working_playlists); 173 getPlaylistCursor(mAdapter.getQueryHandler(), null); 174 } 175 } 176 } 177 178 @Override 179 public Object onRetainNonConfigurationInstance() { 180 PlaylistListAdapter a = mAdapter; 181 mAdapterSent = true; 182 return a; 183 } 184 185 @Override 186 public void onDestroy() { 187 ListView lv = getListView(); 188 if (lv != null) { 189 mLastListPosCourse = lv.getFirstVisiblePosition(); 190 View cv = lv.getChildAt(0); 191 if (cv != null) { 192 mLastListPosFine = cv.getTop(); 193 } 194 } 195 MusicUtils.unbindFromService(mToken); 196 // If we have an adapter and didn't send it off to another activity yet, we should 197 // close its cursor, which we do by assigning a null cursor to it. Doing this 198 // instead of closing the cursor directly keeps the framework from accessing 199 // the closed cursor later. 200 if (!mAdapterSent && mAdapter != null) { 201 mAdapter.changeCursor(null); 202 } 203 // Because we pass the adapter to the next activity, we need to make 204 // sure it doesn't keep a reference to this activity. We can do this 205 // by clearing its DatasetObservers, which setListAdapter(null) does. 206 setListAdapter(null); 207 mAdapter = null; 208 unregisterReceiver(mScanListener); 209 super.onDestroy(); 210 } 211 212 @Override 213 public void onResume() { 214 super.onResume(); 215 216 MusicUtils.setSpinnerState(this); 217 MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this); 218 } 219 @Override 220 public void onPause() { 221 mReScanHandler.removeCallbacksAndMessages(null); 222 super.onPause(); 223 } 224 private BroadcastReceiver mScanListener = new BroadcastReceiver() { 225 @Override 226 public void onReceive(Context context, Intent intent) { 227 MusicUtils.setSpinnerState(PlaylistBrowserActivity.this); 228 mReScanHandler.sendEmptyMessage(0); 229 } 230 }; 231 232 private Handler mReScanHandler = new Handler() { 233 @Override 234 public void handleMessage(Message msg) { 235 if (mAdapter != null) { 236 getPlaylistCursor(mAdapter.getQueryHandler(), null); 237 } 238 } 239 }; 240 public void init(Cursor cursor) { 241 242 if (mAdapter == null) { 243 return; 244 } 245 mAdapter.changeCursor(cursor); 246 247 if (mPlaylistCursor == null) { 248 MusicUtils.displayDatabaseError(this); 249 closeContextMenu(); 250 mReScanHandler.sendEmptyMessageDelayed(0, 1000); 251 return; 252 } 253 254 // restore previous position 255 if (mLastListPosCourse >= 0) { 256 getListView().setSelectionFromTop(mLastListPosCourse, mLastListPosFine); 257 mLastListPosCourse = -1; 258 } 259 MusicUtils.hideDatabaseError(this); 260 MusicUtils.updateButtonBar(this, R.id.playlisttab); 261 setTitle(); 262 } 263 264 private void setTitle() { 265 setTitle(R.string.playlists_title); 266 } 267 268 @Override 269 public boolean onCreateOptionsMenu(Menu menu) { 270 if (!mCreateShortcut) { 271 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 272 } 273 return super.onCreateOptionsMenu(menu); 274 } 275 276 @Override 277 public boolean onPrepareOptionsMenu(Menu menu) { 278 MusicUtils.setPartyShuffleMenuIcon(menu); 279 return super.onPrepareOptionsMenu(menu); 280 } 281 282 @Override 283 public boolean onOptionsItemSelected(MenuItem item) { 284 Intent intent; 285 switch (item.getItemId()) { 286 case PARTY_SHUFFLE: 287 MusicUtils.togglePartyShuffle(); 288 break; 289 } 290 return super.onOptionsItemSelected(item); 291 } 292 293 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { 294 if (mCreateShortcut) { 295 return; 296 } 297 298 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn; 299 300 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection); 301 302 if (mi.id >= 0 /*|| mi.id == PODCASTS_PLAYLIST*/) { 303 menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu); 304 } 305 306 if (mi.id == RECENTLY_ADDED_PLAYLIST) { 307 menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu); 308 } 309 310 if (mi.id >= 0) { 311 menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu); 312 } 313 314 mPlaylistCursor.moveToPosition(mi.position); 315 menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndexOrThrow( 316 MediaStore.Audio.Playlists.NAME))); 317 } 318 319 @Override 320 public boolean onContextItemSelected(MenuItem item) { 321 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) item.getMenuInfo(); 322 switch (item.getItemId()) { 323 case PLAY_SELECTION: 324 if (mi.id == RECENTLY_ADDED_PLAYLIST) { 325 playRecentlyAdded(); 326 } else if (mi.id == PODCASTS_PLAYLIST) { 327 playPodcasts(); 328 } else { 329 MusicUtils.playPlaylist(this, mi.id); 330 } 331 break; 332 case DELETE_PLAYLIST: 333 Uri uri = ContentUris.withAppendedId( 334 MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mi.id); 335 getContentResolver().delete(uri, null, null); 336 Toast.makeText(this, R.string.playlist_deleted_message, Toast.LENGTH_SHORT).show(); 337 if (mPlaylistCursor.getCount() == 0) { 338 setTitle(R.string.no_playlists_title); 339 } 340 break; 341 case EDIT_PLAYLIST: 342 if (mi.id == RECENTLY_ADDED_PLAYLIST) { 343 Intent intent = new Intent(); 344 intent.setClass(this, WeekSelector.class); 345 startActivityForResult(intent, CHANGE_WEEKS); 346 return true; 347 } else { 348 Log.e(TAG, "should not be here"); 349 } 350 break; 351 case RENAME_PLAYLIST: 352 Intent intent = new Intent(); 353 intent.setClass(this, RenamePlaylist.class); 354 intent.putExtra("rename", mi.id); 355 startActivityForResult(intent, RENAME_PLAYLIST); 356 break; 357 } 358 return true; 359 } 360 361 @Override 362 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 363 switch (requestCode) { 364 case SCAN_DONE: 365 if (resultCode == RESULT_CANCELED) { 366 finish(); 367 } else if (mAdapter != null) { 368 getPlaylistCursor(mAdapter.getQueryHandler(), null); 369 } 370 break; 371 } 372 } 373 374 @Override 375 protected void onListItemClick(ListView l, View v, int position, long id) 376 { 377 if (mCreateShortcut) { 378 final Intent shortcut = new Intent(); 379 shortcut.setAction(Intent.ACTION_VIEW); 380 shortcut.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/playlist"); 381 shortcut.putExtra("playlist", String.valueOf(id)); 382 383 final Intent intent = new Intent(); 384 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut); 385 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, ((TextView) v.findViewById(R.id.line1)).getText()); 386 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext( 387 this, R.drawable.ic_launcher_shortcut_music_playlist)); 388 389 setResult(RESULT_OK, intent); 390 finish(); 391 return; 392 } 393 if (id == RECENTLY_ADDED_PLAYLIST) { 394 Intent intent = new Intent(Intent.ACTION_PICK); 395 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 396 intent.putExtra("playlist", "recentlyadded"); 397 startActivity(intent); 398 } else if (id == PODCASTS_PLAYLIST) { 399 Intent intent = new Intent(Intent.ACTION_PICK); 400 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 401 intent.putExtra("playlist", "podcasts"); 402 startActivity(intent); 403 } else { 404 Intent intent = new Intent(Intent.ACTION_EDIT); 405 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); 406 intent.putExtra("playlist", Long.valueOf(id).toString()); 407 startActivity(intent); 408 } 409 } 410 411 private void playRecentlyAdded() { 412 // do a query for all songs added in the last X weeks 413 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7); 414 final String[] ccols = new String[] { MediaStore.Audio.Media._ID}; 415 String where = MediaStore.MediaColumns.DATE_ADDED + ">" + (System.currentTimeMillis() / 1000 - X); 416 Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 417 ccols, where, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 418 419 if (cursor == null) { 420 // Todo: show a message 421 return; 422 } 423 try { 424 int len = cursor.getCount(); 425 long [] list = new long[len]; 426 for (int i = 0; i < len; i++) { 427 cursor.moveToNext(); 428 list[i] = cursor.getLong(0); 429 } 430 MusicUtils.playAll(this, list, 0); 431 } catch (SQLiteException ex) { 432 } finally { 433 cursor.close(); 434 } 435 } 436 437 private void playPodcasts() { 438 // do a query for all files that are podcasts 439 final String[] ccols = new String[] { MediaStore.Audio.Media._ID}; 440 Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 441 ccols, MediaStore.Audio.Media.IS_PODCAST + "=1", 442 null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); 443 444 if (cursor == null) { 445 // Todo: show a message 446 return; 447 } 448 try { 449 int len = cursor.getCount(); 450 long [] list = new long[len]; 451 for (int i = 0; i < len; i++) { 452 cursor.moveToNext(); 453 list[i] = cursor.getLong(0); 454 } 455 MusicUtils.playAll(this, list, 0); 456 } catch (SQLiteException ex) { 457 } finally { 458 cursor.close(); 459 } 460 } 461 462 463 String[] mCols = new String[] { 464 MediaStore.Audio.Playlists._ID, 465 MediaStore.Audio.Playlists.NAME 466 }; 467 468 private Cursor getPlaylistCursor(AsyncQueryHandler async, String filterstring) { 469 470 StringBuilder where = new StringBuilder(); 471 where.append(MediaStore.Audio.Playlists.NAME + " != ''"); 472 473 // Add in the filtering constraints 474 String [] keywords = null; 475 if (filterstring != null) { 476 String [] searchWords = filterstring.split(" "); 477 keywords = new String[searchWords.length]; 478 Collator col = Collator.getInstance(); 479 col.setStrength(Collator.PRIMARY); 480 for (int i = 0; i < searchWords.length; i++) { 481 keywords[i] = '%' + searchWords[i] + '%'; 482 } 483 for (int i = 0; i < searchWords.length; i++) { 484 where.append(" AND "); 485 where.append(MediaStore.Audio.Playlists.NAME + " LIKE ?"); 486 } 487 } 488 489 String whereclause = where.toString(); 490 491 492 if (async != null) { 493 async.startQuery(0, null, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, 494 mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME); 495 return null; 496 } 497 Cursor c = null; 498 c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, 499 mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME); 500 501 return mergedCursor(c); 502 } 503 504 private Cursor mergedCursor(Cursor c) { 505 if (c == null) { 506 return null; 507 } 508 if (c instanceof MergeCursor) { 509 // this shouldn't happen, but fail gracefully 510 Log.d("PlaylistBrowserActivity", "Already wrapped"); 511 return c; 512 } 513 MatrixCursor autoplaylistscursor = new MatrixCursor(mCols); 514 if (mCreateShortcut) { 515 ArrayList<Object> all = new ArrayList<Object>(2); 516 all.add(ALL_SONGS_PLAYLIST); 517 all.add(getString(R.string.play_all)); 518 autoplaylistscursor.addRow(all); 519 } 520 ArrayList<Object> recent = new ArrayList<Object>(2); 521 recent.add(RECENTLY_ADDED_PLAYLIST); 522 recent.add(getString(R.string.recentlyadded)); 523 autoplaylistscursor.addRow(recent); 524 525 // check if there are any podcasts 526 Cursor counter = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 527 new String[] {"count(*)"}, "is_podcast=1", null, null); 528 if (counter != null) { 529 counter.moveToFirst(); 530 int numpodcasts = counter.getInt(0); 531 counter.close(); 532 if (numpodcasts > 0) { 533 ArrayList<Object> podcasts = new ArrayList<Object>(2); 534 podcasts.add(PODCASTS_PLAYLIST); 535 podcasts.add(getString(R.string.podcasts_listitem)); 536 autoplaylistscursor.addRow(podcasts); 537 } 538 } 539 540 Cursor cc = new MergeCursor(new Cursor [] {autoplaylistscursor, c}); 541 return cc; 542 } 543 544 static class PlaylistListAdapter extends SimpleCursorAdapter { 545 int mTitleIdx; 546 int mIdIdx; 547 private PlaylistBrowserActivity mActivity = null; 548 private AsyncQueryHandler mQueryHandler; 549 private String mConstraint = null; 550 private boolean mConstraintIsValid = false; 551 552 class QueryHandler extends AsyncQueryHandler { 553 QueryHandler(ContentResolver res) { 554 super(res); 555 } 556 557 @Override 558 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 559 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity); 560 if (cursor != null) { 561 cursor = mActivity.mergedCursor(cursor); 562 } 563 mActivity.init(cursor); 564 } 565 } 566 567 PlaylistListAdapter(Context context, PlaylistBrowserActivity currentactivity, 568 int layout, Cursor cursor, String[] from, int[] to) { 569 super(context, layout, cursor, from, to); 570 mActivity = currentactivity; 571 getColumnIndices(cursor); 572 mQueryHandler = new QueryHandler(context.getContentResolver()); 573 } 574 private void getColumnIndices(Cursor cursor) { 575 if (cursor != null) { 576 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME); 577 mIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID); 578 } 579 } 580 581 public void setActivity(PlaylistBrowserActivity newactivity) { 582 mActivity = newactivity; 583 } 584 585 public AsyncQueryHandler getQueryHandler() { 586 return mQueryHandler; 587 } 588 589 @Override 590 public void bindView(View view, Context context, Cursor cursor) { 591 592 TextView tv = (TextView) view.findViewById(R.id.line1); 593 594 String name = cursor.getString(mTitleIdx); 595 tv.setText(name); 596 597 long id = cursor.getLong(mIdIdx); 598 599 ImageView iv = (ImageView) view.findViewById(R.id.icon); 600 if (id == RECENTLY_ADDED_PLAYLIST) { 601 iv.setImageResource(R.drawable.ic_mp_playlist_recently_added_list); 602 } else { 603 iv.setImageResource(R.drawable.ic_mp_playlist_list); 604 } 605 ViewGroup.LayoutParams p = iv.getLayoutParams(); 606 p.width = ViewGroup.LayoutParams.WRAP_CONTENT; 607 p.height = ViewGroup.LayoutParams.WRAP_CONTENT; 608 609 iv = (ImageView) view.findViewById(R.id.play_indicator); 610 iv.setVisibility(View.GONE); 611 612 view.findViewById(R.id.line2).setVisibility(View.GONE); 613 } 614 615 @Override 616 public void changeCursor(Cursor cursor) { 617 if (mActivity.isFinishing() && cursor != null) { 618 cursor.close(); 619 cursor = null; 620 } 621 if (cursor != mActivity.mPlaylistCursor) { 622 mActivity.mPlaylistCursor = cursor; 623 super.changeCursor(cursor); 624 getColumnIndices(cursor); 625 } 626 } 627 628 @Override 629 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 630 String s = constraint.toString(); 631 if (mConstraintIsValid && ( 632 (s == null && mConstraint == null) || 633 (s != null && s.equals(mConstraint)))) { 634 return getCursor(); 635 } 636 Cursor c = mActivity.getPlaylistCursor(null, s); 637 mConstraint = s; 638 mConstraintIsValid = true; 639 return c; 640 } 641 } 642 643 private Cursor mPlaylistCursor; 644 } 645 646