/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.music; import com.android.music.MusicUtils.ServiceToken; import android.app.ListActivity; import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.database.sqlite.SQLiteException; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.provider.MediaStore; import android.util.Log; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.ContextMenu.ContextMenuInfo; import android.widget.ImageView; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; import java.text.Collator; import java.util.ArrayList; public class PlaylistBrowserActivity extends ListActivity implements View.OnCreateContextMenuListener, MusicUtils.Defs { private static final String TAG = "PlaylistBrowserActivity"; private static final int DELETE_PLAYLIST = CHILD_MENU_BASE + 1; private static final int EDIT_PLAYLIST = CHILD_MENU_BASE + 2; private static final int RENAME_PLAYLIST = CHILD_MENU_BASE + 3; private static final int CHANGE_WEEKS = CHILD_MENU_BASE + 4; private static final long RECENTLY_ADDED_PLAYLIST = -1; private static final long ALL_SONGS_PLAYLIST = -2; private static final long PODCASTS_PLAYLIST = -3; private PlaylistListAdapter mAdapter; boolean mAdapterSent; private static int mLastListPosCourse = -1; private static int mLastListPosFine = -1; private boolean mCreateShortcut; private ServiceToken mToken; public PlaylistBrowserActivity() { } /** Called when the activity is first created. */ @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); final Intent intent = getIntent(); final String action = intent.getAction(); if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { mCreateShortcut = true; } requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); requestWindowFeature(Window.FEATURE_NO_TITLE); setVolumeControlStream(AudioManager.STREAM_MUSIC); mToken = MusicUtils.bindToService(this, new ServiceConnection() { public void onServiceConnected(ComponentName classname, IBinder obj) { if (Intent.ACTION_VIEW.equals(action)) { Bundle b = intent.getExtras(); if (b == null) { Log.w(TAG, "Unexpected:getExtras() returns null."); } else { try { long id = Long.parseLong(b.getString("playlist")); if (id == RECENTLY_ADDED_PLAYLIST) { playRecentlyAdded(); } else if (id == PODCASTS_PLAYLIST) { playPodcasts(); } else if (id == ALL_SONGS_PLAYLIST) { long[] list = MusicUtils.getAllSongs(PlaylistBrowserActivity.this); if (list != null) { MusicUtils.playAll(PlaylistBrowserActivity.this, list, 0); } } else { MusicUtils.playPlaylist(PlaylistBrowserActivity.this, id); } } catch (NumberFormatException e) { Log.w(TAG, "Playlist id missing or broken"); } } finish(); return; } MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this); } public void onServiceDisconnected(ComponentName classname) { } }); IntentFilter f = new IntentFilter(); f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED); f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED); f.addAction(Intent.ACTION_MEDIA_UNMOUNTED); f.addDataScheme("file"); registerReceiver(mScanListener, f); setContentView(R.layout.media_picker_activity); MusicUtils.updateButtonBar(this, R.id.playlisttab); ListView lv = getListView(); lv.setOnCreateContextMenuListener(this); lv.setTextFilterEnabled(true); mAdapter = (PlaylistListAdapter) getLastNonConfigurationInstance(); if (mAdapter == null) { //Log.i("@@@", "starting query"); mAdapter = new PlaylistListAdapter( getApplication(), this, R.layout.track_list_item, mPlaylistCursor, new String[] { MediaStore.Audio.Playlists.NAME}, new int[] { android.R.id.text1 }); setListAdapter(mAdapter); setTitle(R.string.working_playlists); getPlaylistCursor(mAdapter.getQueryHandler(), null); } else { mAdapter.setActivity(this); setListAdapter(mAdapter); mPlaylistCursor = mAdapter.getCursor(); // If mPlaylistCursor is null, this can be because it doesn't have // a cursor yet (because the initial query that sets its cursor // is still in progress), or because the query failed. // In order to not flash the error dialog at the user for the // first case, simply retry the query when the cursor is null. // Worst case, we end up doing the same query twice. if (mPlaylistCursor != null) { init(mPlaylistCursor); } else { setTitle(R.string.working_playlists); getPlaylistCursor(mAdapter.getQueryHandler(), null); } } } @Override public Object onRetainNonConfigurationInstance() { PlaylistListAdapter a = mAdapter; mAdapterSent = true; return a; } @Override public void onDestroy() { ListView lv = getListView(); if (lv != null) { mLastListPosCourse = lv.getFirstVisiblePosition(); View cv = lv.getChildAt(0); if (cv != null) { mLastListPosFine = cv.getTop(); } } MusicUtils.unbindFromService(mToken); // If we have an adapter and didn't send it off to another activity yet, we should // close its cursor, which we do by assigning a null cursor to it. Doing this // instead of closing the cursor directly keeps the framework from accessing // the closed cursor later. if (!mAdapterSent && mAdapter != null) { mAdapter.changeCursor(null); } // Because we pass the adapter to the next activity, we need to make // sure it doesn't keep a reference to this activity. We can do this // by clearing its DatasetObservers, which setListAdapter(null) does. setListAdapter(null); mAdapter = null; unregisterReceiver(mScanListener); super.onDestroy(); } @Override public void onResume() { super.onResume(); MusicUtils.setSpinnerState(this); MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this); } @Override public void onPause() { mReScanHandler.removeCallbacksAndMessages(null); super.onPause(); } private BroadcastReceiver mScanListener = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { MusicUtils.setSpinnerState(PlaylistBrowserActivity.this); mReScanHandler.sendEmptyMessage(0); } }; private Handler mReScanHandler = new Handler() { @Override public void handleMessage(Message msg) { if (mAdapter != null) { getPlaylistCursor(mAdapter.getQueryHandler(), null); } } }; public void init(Cursor cursor) { if (mAdapter == null) { return; } mAdapter.changeCursor(cursor); if (mPlaylistCursor == null) { MusicUtils.displayDatabaseError(this); closeContextMenu(); mReScanHandler.sendEmptyMessageDelayed(0, 1000); return; } // restore previous position if (mLastListPosCourse >= 0) { getListView().setSelectionFromTop(mLastListPosCourse, mLastListPosFine); mLastListPosCourse = -1; } MusicUtils.hideDatabaseError(this); MusicUtils.updateButtonBar(this, R.id.playlisttab); setTitle(); } private void setTitle() { setTitle(R.string.playlists_title); } @Override public boolean onCreateOptionsMenu(Menu menu) { if (!mCreateShortcut) { menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() } return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { MusicUtils.setPartyShuffleMenuIcon(menu); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { Intent intent; switch (item.getItemId()) { case PARTY_SHUFFLE: MusicUtils.togglePartyShuffle(); break; } return super.onOptionsItemSelected(item); } public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) { if (mCreateShortcut) { return; } AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn; menu.add(0, PLAY_SELECTION, 0, R.string.play_selection); if (mi.id >= 0 /*|| mi.id == PODCASTS_PLAYLIST*/) { menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu); } if (mi.id == RECENTLY_ADDED_PLAYLIST) { menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu); } if (mi.id >= 0) { menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu); } mPlaylistCursor.moveToPosition(mi.position); menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndexOrThrow( MediaStore.Audio.Playlists.NAME))); } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo mi = (AdapterContextMenuInfo) item.getMenuInfo(); switch (item.getItemId()) { case PLAY_SELECTION: if (mi.id == RECENTLY_ADDED_PLAYLIST) { playRecentlyAdded(); } else if (mi.id == PODCASTS_PLAYLIST) { playPodcasts(); } else { MusicUtils.playPlaylist(this, mi.id); } break; case DELETE_PLAYLIST: Uri uri = ContentUris.withAppendedId( MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mi.id); getContentResolver().delete(uri, null, null); Toast.makeText(this, R.string.playlist_deleted_message, Toast.LENGTH_SHORT).show(); if (mPlaylistCursor.getCount() == 0) { setTitle(R.string.no_playlists_title); } break; case EDIT_PLAYLIST: if (mi.id == RECENTLY_ADDED_PLAYLIST) { Intent intent = new Intent(); intent.setClass(this, WeekSelector.class); startActivityForResult(intent, CHANGE_WEEKS); return true; } else { Log.e(TAG, "should not be here"); } break; case RENAME_PLAYLIST: Intent intent = new Intent(); intent.setClass(this, RenamePlaylist.class); intent.putExtra("rename", mi.id); startActivityForResult(intent, RENAME_PLAYLIST); break; } return true; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { switch (requestCode) { case SCAN_DONE: if (resultCode == RESULT_CANCELED) { finish(); } else if (mAdapter != null) { getPlaylistCursor(mAdapter.getQueryHandler(), null); } break; } } @Override protected void onListItemClick(ListView l, View v, int position, long id) { if (mCreateShortcut) { final Intent shortcut = new Intent(); shortcut.setAction(Intent.ACTION_VIEW); shortcut.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/playlist"); shortcut.putExtra("playlist", String.valueOf(id)); final Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut); intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, ((TextView) v.findViewById(R.id.line1)).getText()); intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext( this, R.drawable.ic_launcher_shortcut_music_playlist)); setResult(RESULT_OK, intent); finish(); return; } if (id == RECENTLY_ADDED_PLAYLIST) { Intent intent = new Intent(Intent.ACTION_PICK); intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); intent.putExtra("playlist", "recentlyadded"); startActivity(intent); } else if (id == PODCASTS_PLAYLIST) { Intent intent = new Intent(Intent.ACTION_PICK); intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); intent.putExtra("playlist", "podcasts"); startActivity(intent); } else { Intent intent = new Intent(Intent.ACTION_EDIT); intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track"); intent.putExtra("playlist", Long.valueOf(id).toString()); startActivity(intent); } } private void playRecentlyAdded() { // do a query for all songs added in the last X weeks int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7); final String[] ccols = new String[] { MediaStore.Audio.Media._ID}; String where = MediaStore.MediaColumns.DATE_ADDED + ">" + (System.currentTimeMillis() / 1000 - X); Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, where, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); if (cursor == null) { // Todo: show a message return; } try { int len = cursor.getCount(); long [] list = new long[len]; for (int i = 0; i < len; i++) { cursor.moveToNext(); list[i] = cursor.getLong(0); } MusicUtils.playAll(this, list, 0); } catch (SQLiteException ex) { } finally { cursor.close(); } } private void playPodcasts() { // do a query for all files that are podcasts final String[] ccols = new String[] { MediaStore.Audio.Media._ID}; Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, ccols, MediaStore.Audio.Media.IS_PODCAST + "=1", null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); if (cursor == null) { // Todo: show a message return; } try { int len = cursor.getCount(); long [] list = new long[len]; for (int i = 0; i < len; i++) { cursor.moveToNext(); list[i] = cursor.getLong(0); } MusicUtils.playAll(this, list, 0); } catch (SQLiteException ex) { } finally { cursor.close(); } } String[] mCols = new String[] { MediaStore.Audio.Playlists._ID, MediaStore.Audio.Playlists.NAME }; private Cursor getPlaylistCursor(AsyncQueryHandler async, String filterstring) { StringBuilder where = new StringBuilder(); where.append(MediaStore.Audio.Playlists.NAME + " != ''"); // Add in the filtering constraints String [] keywords = null; if (filterstring != null) { String [] searchWords = filterstring.split(" "); keywords = new String[searchWords.length]; Collator col = Collator.getInstance(); col.setStrength(Collator.PRIMARY); for (int i = 0; i < searchWords.length; i++) { keywords[i] = '%' + searchWords[i] + '%'; } for (int i = 0; i < searchWords.length; i++) { where.append(" AND "); where.append(MediaStore.Audio.Playlists.NAME + " LIKE ?"); } } String whereclause = where.toString(); if (async != null) { async.startQuery(0, null, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME); return null; } Cursor c = null; c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME); return mergedCursor(c); } private Cursor mergedCursor(Cursor c) { if (c == null) { return null; } if (c instanceof MergeCursor) { // this shouldn't happen, but fail gracefully Log.d("PlaylistBrowserActivity", "Already wrapped"); return c; } MatrixCursor autoplaylistscursor = new MatrixCursor(mCols); if (mCreateShortcut) { ArrayList<Object> all = new ArrayList<Object>(2); all.add(ALL_SONGS_PLAYLIST); all.add(getString(R.string.play_all)); autoplaylistscursor.addRow(all); } ArrayList<Object> recent = new ArrayList<Object>(2); recent.add(RECENTLY_ADDED_PLAYLIST); recent.add(getString(R.string.recentlyadded)); autoplaylistscursor.addRow(recent); // check if there are any podcasts Cursor counter = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] {"count(*)"}, "is_podcast=1", null, null); if (counter != null) { counter.moveToFirst(); int numpodcasts = counter.getInt(0); counter.close(); if (numpodcasts > 0) { ArrayList<Object> podcasts = new ArrayList<Object>(2); podcasts.add(PODCASTS_PLAYLIST); podcasts.add(getString(R.string.podcasts_listitem)); autoplaylistscursor.addRow(podcasts); } } Cursor cc = new MergeCursor(new Cursor [] {autoplaylistscursor, c}); return cc; } static class PlaylistListAdapter extends SimpleCursorAdapter { int mTitleIdx; int mIdIdx; private PlaylistBrowserActivity mActivity = null; private AsyncQueryHandler mQueryHandler; private String mConstraint = null; private boolean mConstraintIsValid = false; class QueryHandler extends AsyncQueryHandler { QueryHandler(ContentResolver res) { super(res); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { //Log.i("@@@", "query complete: " + cursor.getCount() + " " + mActivity); if (cursor != null) { cursor = mActivity.mergedCursor(cursor); } mActivity.init(cursor); } } PlaylistListAdapter(Context context, PlaylistBrowserActivity currentactivity, int layout, Cursor cursor, String[] from, int[] to) { super(context, layout, cursor, from, to); mActivity = currentactivity; getColumnIndices(cursor); mQueryHandler = new QueryHandler(context.getContentResolver()); } private void getColumnIndices(Cursor cursor) { if (cursor != null) { mTitleIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists.NAME); mIdIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Playlists._ID); } } public void setActivity(PlaylistBrowserActivity newactivity) { mActivity = newactivity; } public AsyncQueryHandler getQueryHandler() { return mQueryHandler; } @Override public void bindView(View view, Context context, Cursor cursor) { TextView tv = (TextView) view.findViewById(R.id.line1); String name = cursor.getString(mTitleIdx); tv.setText(name); long id = cursor.getLong(mIdIdx); ImageView iv = (ImageView) view.findViewById(R.id.icon); if (id == RECENTLY_ADDED_PLAYLIST) { iv.setImageResource(R.drawable.ic_mp_playlist_recently_added_list); } else { iv.setImageResource(R.drawable.ic_mp_playlist_list); } ViewGroup.LayoutParams p = iv.getLayoutParams(); p.width = ViewGroup.LayoutParams.WRAP_CONTENT; p.height = ViewGroup.LayoutParams.WRAP_CONTENT; iv = (ImageView) view.findViewById(R.id.play_indicator); iv.setVisibility(View.GONE); view.findViewById(R.id.line2).setVisibility(View.GONE); } @Override public void changeCursor(Cursor cursor) { if (mActivity.isFinishing() && cursor != null) { cursor.close(); cursor = null; } if (cursor != mActivity.mPlaylistCursor) { mActivity.mPlaylistCursor = cursor; super.changeCursor(cursor); getColumnIndices(cursor); } } @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint) { String s = constraint.toString(); if (mConstraintIsValid && ( (s == null && mConstraint == null) || (s != null && s.equals(mConstraint)))) { return getCursor(); } Cursor c = mActivity.getPlaylistCursor(null, s); mConstraint = s; mConstraintIsValid = true; return c; } } private Cursor mPlaylistCursor; }