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