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