PlaylistBrowserActivity.java revision 9e0f1fde0956ee7f5b76b77df7aea9b936e2c3a9
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.internal.database.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            mLastListPosFine = lv.getChildAt(0).getTop();
181        }
182        MusicUtils.unbindFromService(this);
183        if (!mAdapterSent) {
184            Cursor c = mAdapter.getCursor();
185            if (c != null) {
186                c.close();
187            }
188        }
189        // Because we pass the adapter to the next activity, we need to make
190        // sure it doesn't keep a reference to this activity. We can do this
191        // by clearing its DatasetObservers, which setListAdapter(null) does.
192        setListAdapter(null);
193        mAdapter = null;
194        unregisterReceiver(mScanListener);
195        super.onDestroy();
196    }
197
198    @Override
199    public void onResume() {
200        super.onResume();
201
202        MusicUtils.setSpinnerState(this);
203        MusicUtils.updateNowPlaying(PlaylistBrowserActivity.this);
204    }
205    @Override
206    public void onPause() {
207        mReScanHandler.removeCallbacksAndMessages(null);
208        super.onPause();
209    }
210    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
211        @Override
212        public void onReceive(Context context, Intent intent) {
213            MusicUtils.setSpinnerState(PlaylistBrowserActivity.this);
214            mReScanHandler.sendEmptyMessage(0);
215        }
216    };
217
218    private Handler mReScanHandler = new Handler() {
219        @Override
220        public void handleMessage(Message msg) {
221            if (mAdapter != null) {
222                getPlaylistCursor(mAdapter.getQueryHandler(), null);
223            }
224        }
225    };
226    public void init(Cursor cursor) {
227
228        if (mAdapter == null) {
229            return;
230        }
231        mAdapter.changeCursor(cursor);
232
233        if (mPlaylistCursor == null) {
234            MusicUtils.displayDatabaseError(this);
235            closeContextMenu();
236            mReScanHandler.sendEmptyMessageDelayed(0, 1000);
237            return;
238        }
239
240        // restore previous position
241        if (mLastListPosCourse >= 0) {
242            getListView().setSelectionFromTop(mLastListPosCourse, mLastListPosFine);
243            mLastListPosCourse = -1;
244        }
245        MusicUtils.hideDatabaseError(this);
246        MusicUtils.updateButtonBar(this, R.id.playlisttab);
247        setTitle();
248    }
249
250    private void setTitle() {
251        setTitle(R.string.playlists_title);
252    }
253
254    @Override
255    public boolean onCreateOptionsMenu(Menu menu) {
256        if (!mCreateShortcut) {
257            menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu()
258        }
259        return super.onCreateOptionsMenu(menu);
260    }
261
262    @Override
263    public boolean onPrepareOptionsMenu(Menu menu) {
264        MusicUtils.setPartyShuffleMenuIcon(menu);
265        return super.onPrepareOptionsMenu(menu);
266    }
267
268    @Override
269    public boolean onOptionsItemSelected(MenuItem item) {
270        Intent intent;
271        switch (item.getItemId()) {
272            case PARTY_SHUFFLE:
273                MusicUtils.togglePartyShuffle();
274                break;
275        }
276        return super.onOptionsItemSelected(item);
277    }
278
279    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
280        if (mCreateShortcut) {
281            return;
282        }
283
284        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
285
286        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
287
288        if (mi.id >= 0 /*|| mi.id == PODCASTS_PLAYLIST*/) {
289            menu.add(0, DELETE_PLAYLIST, 0, R.string.delete_playlist_menu);
290        }
291
292        if (mi.id == RECENTLY_ADDED_PLAYLIST) {
293            menu.add(0, EDIT_PLAYLIST, 0, R.string.edit_playlist_menu);
294        }
295
296        if (mi.id >= 0) {
297            menu.add(0, RENAME_PLAYLIST, 0, R.string.rename_playlist_menu);
298        }
299
300        mPlaylistCursor.moveToPosition(mi.position);
301        menu.setHeaderTitle(mPlaylistCursor.getString(mPlaylistCursor.getColumnIndexOrThrow(
302                MediaStore.Audio.Playlists.NAME)));
303    }
304
305    @Override
306    public boolean onContextItemSelected(MenuItem item) {
307        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) item.getMenuInfo();
308        switch (item.getItemId()) {
309            case PLAY_SELECTION:
310                if (mi.id == RECENTLY_ADDED_PLAYLIST) {
311                    playRecentlyAdded();
312                } else if (mi.id == PODCASTS_PLAYLIST) {
313                    playPodcasts();
314                } else {
315                    MusicUtils.playPlaylist(this, mi.id);
316                }
317                break;
318            case DELETE_PLAYLIST:
319                Uri uri = ContentUris.withAppendedId(
320                        MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, mi.id);
321                getContentResolver().delete(uri, null, null);
322                Toast.makeText(this, R.string.playlist_deleted_message, Toast.LENGTH_SHORT).show();
323                if (mPlaylistCursor.getCount() == 0) {
324                    setTitle(R.string.no_playlists_title);
325                }
326                break;
327            case EDIT_PLAYLIST:
328                if (mi.id == RECENTLY_ADDED_PLAYLIST) {
329                    Intent intent = new Intent();
330                    intent.setClass(this, WeekSelector.class);
331                    startActivityForResult(intent, CHANGE_WEEKS);
332                    return true;
333                } else {
334                    Log.e(TAG, "should not be here");
335                }
336                break;
337            case RENAME_PLAYLIST:
338                Intent intent = new Intent();
339                intent.setClass(this, RenamePlaylist.class);
340                intent.putExtra("rename", mi.id);
341                startActivityForResult(intent, RENAME_PLAYLIST);
342                break;
343        }
344        return true;
345    }
346
347    @Override
348    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
349        switch (requestCode) {
350            case SCAN_DONE:
351                if (resultCode == RESULT_CANCELED) {
352                    finish();
353                } else if (mAdapter != null) {
354                    getPlaylistCursor(mAdapter.getQueryHandler(), null);
355                }
356                break;
357        }
358    }
359
360    @Override
361    protected void onListItemClick(ListView l, View v, int position, long id)
362    {
363        if (mCreateShortcut) {
364            final Intent shortcut = new Intent();
365            shortcut.setAction(Intent.ACTION_VIEW);
366            shortcut.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/playlist");
367            shortcut.putExtra("playlist", String.valueOf(id));
368
369            final Intent intent = new Intent();
370            intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcut);
371            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, ((TextView) v.findViewById(R.id.line1)).getText());
372            intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(
373                    this, R.drawable.ic_launcher_shortcut_music_playlist));
374
375            setResult(RESULT_OK, intent);
376            finish();
377            return;
378        }
379        if (id == RECENTLY_ADDED_PLAYLIST) {
380            Intent intent = new Intent(Intent.ACTION_PICK);
381            intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
382            intent.putExtra("playlist", "recentlyadded");
383            startActivity(intent);
384        } else if (id == PODCASTS_PLAYLIST) {
385            Intent intent = new Intent(Intent.ACTION_PICK);
386            intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
387            intent.putExtra("playlist", "podcasts");
388            startActivity(intent);
389        } else {
390            Intent intent = new Intent(Intent.ACTION_EDIT);
391            intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
392            intent.putExtra("playlist", Long.valueOf(id).toString());
393            startActivity(intent);
394        }
395    }
396
397    private void playRecentlyAdded() {
398        // do a query for all songs added in the last X weeks
399        int X = MusicUtils.getIntPref(this, "numweeks", 2) * (3600 * 24 * 7);
400        final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
401        String where = MediaStore.MediaColumns.DATE_ADDED + ">" + (System.currentTimeMillis() / 1000 - X);
402        Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
403                ccols, where, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
404
405        if (cursor == null) {
406            // Todo: show a message
407            return;
408        }
409        try {
410            int len = cursor.getCount();
411            long [] list = new long[len];
412            for (int i = 0; i < len; i++) {
413                cursor.moveToNext();
414                list[i] = cursor.getLong(0);
415            }
416            MusicUtils.playAll(this, list, 0);
417        } catch (SQLiteException ex) {
418        } finally {
419            cursor.close();
420        }
421    }
422
423    private void playPodcasts() {
424        // do a query for all files that are podcasts
425        final String[] ccols = new String[] { MediaStore.Audio.Media._ID};
426        Cursor cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
427                ccols, MediaStore.Audio.Media.IS_PODCAST + "=1",
428                null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
429
430        if (cursor == null) {
431            // Todo: show a message
432            return;
433        }
434        try {
435            int len = cursor.getCount();
436            long [] list = new long[len];
437            for (int i = 0; i < len; i++) {
438                cursor.moveToNext();
439                list[i] = cursor.getLong(0);
440            }
441            MusicUtils.playAll(this, list, 0);
442        } catch (SQLiteException ex) {
443        } finally {
444            cursor.close();
445        }
446    }
447
448
449    String[] mCols = new String[] {
450            MediaStore.Audio.Playlists._ID,
451            MediaStore.Audio.Playlists.NAME
452    };
453
454    private Cursor getPlaylistCursor(AsyncQueryHandler async, String filterstring) {
455
456        StringBuilder where = new StringBuilder();
457        where.append(MediaStore.Audio.Playlists.NAME + " != ''");
458
459        // Add in the filtering constraints
460        String [] keywords = null;
461        if (filterstring != null) {
462            String [] searchWords = filterstring.split(" ");
463            keywords = new String[searchWords.length];
464            Collator col = Collator.getInstance();
465            col.setStrength(Collator.PRIMARY);
466            for (int i = 0; i < searchWords.length; i++) {
467                keywords[i] = '%' + searchWords[i] + '%';
468            }
469            for (int i = 0; i < searchWords.length; i++) {
470                where.append(" AND ");
471                where.append(MediaStore.Audio.Playlists.NAME + " LIKE ?");
472            }
473        }
474
475        String whereclause = where.toString();
476
477
478        if (async != null) {
479            async.startQuery(0, null, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
480                    mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
481            return null;
482        }
483        Cursor c = null;
484        c = MusicUtils.query(this, MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
485                mCols, whereclause, keywords, MediaStore.Audio.Playlists.NAME);
486
487        return mergedCursor(c);
488    }
489
490    private Cursor mergedCursor(Cursor c) {
491        if (c == null) {
492            return null;
493        }
494        if (c instanceof MergeCursor) {
495            // this shouldn't happen, but fail gracefully
496            Log.d("PlaylistBrowserActivity", "Already wrapped");
497            return c;
498        }
499        ArrayList<ArrayList> autoplaylists = new ArrayList<ArrayList>();
500        if (mCreateShortcut) {
501            ArrayList<Object> all = new ArrayList<Object>(2);
502            all.add(ALL_SONGS_PLAYLIST);
503            all.add(getString(R.string.play_all));
504            autoplaylists.add(all);
505        }
506        ArrayList<Object> recent = new ArrayList<Object>(2);
507        recent.add(RECENTLY_ADDED_PLAYLIST);
508        recent.add(getString(R.string.recentlyadded));
509        autoplaylists.add(recent);
510
511        // check if there are any podcasts
512        Cursor counter = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
513                new String[] {"count(*)"}, "is_podcast=1", null, null);
514        if (counter != null) {
515            counter.moveToFirst();
516            int numpodcasts = counter.getInt(0);
517            counter.close();
518            if (numpodcasts > 0) {
519                ArrayList<Object> podcasts = new ArrayList<Object>(2);
520                podcasts.add(PODCASTS_PLAYLIST);
521                podcasts.add(getString(R.string.podcasts_listitem));
522                autoplaylists.add(podcasts);
523            }
524        }
525
526        ArrayListCursor autoplaylistscursor = new ArrayListCursor(mCols, autoplaylists);
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 (cursor != mActivity.mPlaylistCursor) {
606                mActivity.mPlaylistCursor = cursor;
607                super.changeCursor(cursor);
608                getColumnIndices(cursor);
609            }
610        }
611
612        @Override
613        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
614            String s = constraint.toString();
615            if (mConstraintIsValid && (
616                    (s == null && mConstraint == null) ||
617                    (s != null && s.equals(mConstraint)))) {
618                return getCursor();
619            }
620            Cursor c = mActivity.getPlaylistCursor(null, s);
621            mConstraint = s;
622            mConstraintIsValid = true;
623            return c;
624        }
625    }
626
627    private Cursor mPlaylistCursor;
628}
629
630