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