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