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