2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.music;
19 import com.android.music.MusicUtils.ServiceToken;
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;
57 import java.text.Collator;
58 import java.util.ArrayList;
60 public class PlaylistBrowserActivity extends ListActivity
61 implements View.OnCreateContextMenuListener, MusicUtils.Defs
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;
73 private static int mLastListPosCourse = -1;
74 private static int mLastListPosFine = -1;
76 private boolean mCreateShortcut;
77 private ServiceToken mToken;
79 public PlaylistBrowserActivity()
83 /** Called when the activity is first created. */
85 public void onCreate(Bundle icicle)
87 super.onCreate(icicle);
89 final Intent intent = getIntent();
90 final String action = intent.getAction();
91 if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
92 mCreateShortcut = true;
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 long id = Long.parseLong(intent.getExtras().getString("playlist"));
102 if (id == RECENTLY_ADDED_PLAYLIST) {
104 } else if (id == PODCASTS_PLAYLIST) {
106 } else if (id == ALL_SONGS_PLAYLIST) {
107 long [] list = MusicUtils.getAllSongs(PlaylistBrowserActivity.this);
109 MusicUtils.playAll(PlaylistBrowserActivity.this, list, 0);
112 MusicUtils.playPlaylist(PlaylistBrowserActivity.this, id);
117 MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this);
120 public void onServiceDisconnected(ComponentName classname) {
124 IntentFilter f = new IntentFilter();
125 f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
126 f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
127 f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
128 f.addDataScheme("file");
129 registerReceiver(mScanListener, f);
131 setContentView(R.layout.media_picker_activity);
132 MusicUtils.updateButtonBar(this, R.id.playlisttab);
133 ListView lv = getListView();
134 lv.setOnCreateContextMenuListener(this);
135 lv.setTextFilterEnabled(true);
137 mAdapter = (PlaylistListAdapter) getLastNonConfigurationInstance();
138 if (mAdapter == null) {
139 //Log.i("@@@", "starting query");
140 mAdapter = new PlaylistListAdapter(
143 R.layout.track_list_item,
145 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);
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);
163 setTitle(R.string.working_playlists);
164 getPlaylistCursor(mAdapter.getQueryHandler(), null);
170 public Object onRetainNonConfigurationInstance() {
171 PlaylistListAdapter a = mAdapter;
177 public void onDestroy() {
178 ListView lv = getListView();
180 mLastListPosCourse = lv.getFirstVisiblePosition();
181 View cv = lv.getChildAt(0);
183 mLastListPosFine = cv.getTop();
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);
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);
199 unregisterReceiver(mScanListener);
204 public void onResume() {
207 MusicUtils.setSpinnerState(this);
208 MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this);
211 public void onPause() {
212 mReScanHandler.removeCallbacksAndMessages(null);
215 private BroadcastReceiver mScanListener = new BroadcastReceiver() {
217 public void onReceive(Context context, Intent intent) {
218 MusicUtils.setSpinnerState(PlaylistBrowserActivity.this);
219 mReScanHandler.sendEmptyMessage(0);
223 private Handler mReScanHandler = new Handler() {
225 public void handleMessage(Message msg) {
226 if (mAdapter != null) {
227 getPlaylistCursor(mAdapter.getQueryHandler(), null);
231 public void init(Cursor cursor) {
233 if (mAdapter == null) {
236 mAdapter.changeCursor(cursor);
238 if (mPlaylistCursor == null) {
239 MusicUtils.displayDatabaseError(this);
241 mReScanHandler.sendEmptyMessageDelayed(0, 1000);
245 // restore previous position
246 if (mLastListPosCourse >= 0) {
247 getListView().setSelectionFromTop(mLastListPosCourse, mLastListPosFine);
248 mLastListPosCourse = -1;
250 MusicUtils.hideDatabaseError(this);
251 MusicUtils.updateButtonBar(this, R.id.playlisttab);
255 private void setTitle() {
256 setTitle(R.string.playlists_title);
260 public boolean onCreateOptionsMenu(Menu menu) {
261 if (!mCreateShortcut) {
262 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
264 return super.onCreateOptionsMenu(menu);
268 public boolean onPrepareOptionsMenu(Menu menu) {
269 MusicUtils.setPartyShuffleMenuIcon(menu);
270 return super.onPrepareOptionsMenu(menu);
274 public boolean onOptionsItemSelected(MenuItem item) {
276 switch (item.getItemId()) {
278 MusicUtils.togglePartyShuffle();
281 return super.onOptionsItemSelected(item);
284 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
285 if (mCreateShortcut) {
289 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
291 menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
293 if (mi.id >= 0 /*|| mi.id == PODCASTS_PLAYLIST*/) {
294 menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu);
297 if (mi.id == RECENTLY_ADDED_PLAYLIST) {
298 menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu);
302 menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu);
305 mPlaylistCursor.moveToPosition(mi.position);
306 menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndexOrThrow(
307 MediaStore.Audio.Playlists.NAME)));
311 public boolean onContextItemSelected(MenuItem item) {
312 AdapterContextMenuInfo mi = (AdapterContextMenuInfo) item.getMenuInfo();
313 switch (item.getItemId()) {
315 if (mi.id == RECENTLY_ADDED_PLAYLIST) {
317 } else if (mi.id == PODCASTS_PLAYLIST) {
320 MusicUtils.playPlaylist(this, mi.id);
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);
333 if (mi.id == RECENTLY_ADDED_PLAYLIST) {
334 Intent intent = new Intent();
335 intent.setClass(this, WeekSelector.class);
336 startActivityForResult(intent, CHANGE_WEEKS);
339 Log.e(TAG, "should not be here");
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);
353 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
354 switch (requestCode) {
356 if (resultCode == RESULT_CANCELED) {
358 } else if (mAdapter != null) {
359 getPlaylistCursor(mAdapter.getQueryHandler(), null);
366 protected void onListItemClick(ListView l, View v, int position, long id)
368 if (mCreateShortcut) {
369 final Intent shortcut = new Intent();
370 shortcut.setAction(Intent.ACTION_VIEW);
371 shortcut.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/playlist");
372 shortcut.putExtra("playlist", String.valueOf(id));
374 final Intent intent = new Intent();
375 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);
376 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, ((TextView) v.findViewById(R.id.line1)).getText());
377 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(
378 this, R.drawable.ic_launcher_shortcut_music_playlist));
380 setResult(RESULT_OK, intent);
384 if (id == RECENTLY_ADDED_PLAYLIST) {
385 Intent intent = new Intent(Intent.ACTION_PICK);
386 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
387 intent.putExtra("playlist", "recentlyadded");
388 startActivity(intent);
389 } else if (id == PODCASTS_PLAYLIST) {
390 Intent intent = new Intent(Intent.ACTION_PICK);
391 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
392 intent.putExtra("playlist", "podcasts");
393 startActivity(intent);
395 Intent intent = new Intent(Intent.ACTION_EDIT);
396 intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
397 intent.putExtra("playlist", Long.valueOf(id).toString());
398 startActivity(intent);
402 private void playRecentlyAdded() {
403 // do a query for all songs added in the last X weeks
404 int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7);
405 final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
406 String where = MediaStore.MediaColumns.DATE_ADDED + ">" + (System.currentTimeMillis() / 1000 - X);
407 Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
408 ccols, where, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
410 if (cursor == null) {
411 // Todo: show a message
415 int len = cursor.getCount();
416 long [] list = new long[len];
417 for (int i = 0; i < len; i++) {
419 list[i] = cursor.getLong(0);
421 MusicUtils.playAll(this, list, 0);
422 } catch (SQLiteException ex) {
428 private void playPodcasts() {
429 // do a query for all files that are podcasts
430 final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
431 Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
432 ccols, MediaStore.Audio.Media.IS_PODCAST + "=1",
433 null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
435 if (cursor == null) {
436 // Todo: show a message
440 int len = cursor.getCount();
441 long [] list = new long[len];
442 for (int i = 0; i < len; i++) {
444 list[i] = cursor.getLong(0);
446 MusicUtils.playAll(this, list, 0);
447 } catch (SQLiteException ex) {
454 String[] mCols = new String[] {
455 MediaStore.Audio.Playlists._ID,
456 MediaStore.Audio.Playlists.NAME
459 private Cursor getPlaylistCursor(AsyncQueryHandler async, String filterstring) {
461 StringBuilder where = new StringBuilder();
462 where.append(MediaStore.Audio.Playlists.NAME + " != ''");
464 // Add in the filtering constraints
465 String [] keywords = null;
466 if (filterstring != null) {
467 String [] searchWords = filterstring.split(" ");
468 keywords = new String[searchWords.length];
469 Collator col = Collator.getInstance();
470 col.setStrength(Collator.PRIMARY);
471 for (int i = 0; i < searchWords.length; i++) {
472 keywords[i] = '%' + searchWords[i] + '%';
474 for (int i = 0; i < searchWords.length; i++) {
475 where.append(" AND ");
476 where.append(MediaStore.Audio.Playlists.NAME + " LIKE ?");
480 String whereclause = where.toString();
484 async.startQuery(0, null, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
485 mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
489 c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
490 mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
492 return mergedCursor(c);
495 private Cursor mergedCursor(Cursor c) {
499 if (c instanceof MergeCursor) {
500 // this shouldn't happen, but fail gracefully
501 Log.d("PlaylistBrowserActivity", "Already wrapped");
504 MatrixCursor autoplaylistscursor = new MatrixCursor(mCols);
505 if (mCreateShortcut) {
506 ArrayList<Object> all = new ArrayList<Object>(2);
507 all.add(ALL_SONGS_PLAYLIST);
508 all.add(getString(R.string.play_all));
509 autoplaylistscursor.addRow(all);
511 ArrayList<Object> recent = new ArrayList<Object>(2);
512 recent.add(RECENTLY_ADDED_PLAYLIST);
513 recent.add(getString(R.string.recentlyadded));
514 autoplaylistscursor.addRow(recent);
516 // check if there are any podcasts
517 Cursor counter = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
518 new String[] {"count(*)"}, "is_podcast=1", null, null);
519 if (counter != null) {
520 counter.moveToFirst();
521 int numpodcasts = counter.getInt(0);
523 if (numpodcasts > 0) {
524 ArrayList<Object> podcasts = new ArrayList<Object>(2);
525 podcasts.add(PODCASTS_PLAYLIST);
526 podcasts.add(getString(R.string.podcasts_listitem));
527 autoplaylistscursor.addRow(podcasts);
531 Cursor cc = new MergeCursor(new Cursor [] {autoplaylistscursor, c});
535 static class PlaylistListAdapter extends SimpleCursorAdapter {
538 private PlaylistBrowserActivity mActivity = null;
539 private AsyncQueryHandler mQueryHandler;
540 private String mConstraint = null;
541 private boolean mConstraintIsValid = false;
543 class QueryHandler extends AsyncQueryHandler {
544 QueryHandler(ContentResolver res) {
549 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
550 //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity);
551 if (cursor != null) {
552 cursor = mActivity.mergedCursor(cursor);
554 mActivity.init(cursor);
558 PlaylistListAdapter(Context context, PlaylistBrowserActivity currentactivity,
559 int layout, Cursor cursor, String[] from, int[] to) {
560 super(context, layout, cursor, from, to);
561 mActivity = currentactivity;
562 getColumnIndices(cursor);
563 mQueryHandler = new QueryHandler(context.getContentResolver());
565 private void getColumnIndices(Cursor cursor) {
566 if (cursor != null) {
567 mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME);
568 mIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID);
572 public void setActivity(PlaylistBrowserActivity newactivity) {
573 mActivity = newactivity;
576 public AsyncQueryHandler getQueryHandler() {
577 return mQueryHandler;
581 public void bindView(View view, Context context, Cursor cursor) {
583 TextView tv = (TextView) view.findViewById(R.id.line1);
585 String name = cursor.getString(mTitleIdx);
588 long id = cursor.getLong(mIdIdx);
590 ImageView iv = (ImageView) view.findViewById(R.id.icon);
591 if (id == RECENTLY_ADDED_PLAYLIST) {
592 iv.setImageResource(R.drawable.ic_mp_playlist_recently_added_list);
594 iv.setImageResource(R.drawable.ic_mp_playlist_list);
596 ViewGroup.LayoutParams p = iv.getLayoutParams();
597 p.width = ViewGroup.LayoutParams.WRAP_CONTENT;
598 p.height = ViewGroup.LayoutParams.WRAP_CONTENT;
600 iv = (ImageView) view.findViewById(R.id.play_indicator);
601 iv.setVisibility(View.GONE);
603 view.findViewById(R.id.line2).setVisibility(View.GONE);
607 public void changeCursor(Cursor cursor) {
608 if (mActivity.isFinishing() && cursor != null) {
612 if (cursor != mActivity.mPlaylistCursor) {
613 mActivity.mPlaylistCursor = cursor;
614 super.changeCursor(cursor);
615 getColumnIndices(cursor);
620 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
621 String s = constraint.toString();
622 if (mConstraintIsValid && (
623 (s == null && mConstraint == null) ||
624 (s != null && s.equals(mConstraint)))) {
627 Cursor c = mActivity.getPlaylistCursor(null, s);
629 mConstraintIsValid = true;
634 private Cursor mPlaylistCursor;