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