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