AlbumBrowserActivity.java revision 0164ebfdcf374bc2b65e7c86d02b597059c25e44
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 android.app.ListActivity;
20import android.app.SearchManager;
21import android.content.AsyncQueryHandler;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.drawable.BitmapDrawable;
32import android.graphics.drawable.Drawable;
33import android.media.AudioManager;
34import android.media.MediaFile;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.os.Message;
39import android.provider.MediaStore;
40import android.view.ContextMenu;
41import android.view.Menu;
42import android.view.MenuItem;
43import android.view.SubMenu;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.Window;
47import android.view.ContextMenu.ContextMenuInfo;
48import android.widget.Adapter;
49import android.widget.AlphabetIndexer;
50import android.widget.CursorAdapter;
51import android.widget.ImageView;
52import android.widget.ListAdapter;
53import android.widget.ListView;
54import android.widget.SectionIndexer;
55import android.widget.SimpleCursorAdapter;
56import android.widget.TextView;
57import android.widget.AdapterView.AdapterContextMenuInfo;
58
59import java.text.Collator;
60
61public class AlbumBrowserActivity extends ListActivity
62    implements View.OnCreateContextMenuListener, MusicUtils.Defs
63{
64    private String mCurrentAlbumId;
65    private String mCurrentAlbumName;
66    private String mCurrentArtistNameForAlbum;
67    private AlbumListAdapter mAdapter;
68    private boolean mAdapterSent;
69    private final static int SEARCH = CHILD_MENU_BASE;
70
71    public AlbumBrowserActivity()
72    {
73    }
74
75    /** Called when the activity is first created. */
76    @Override
77    public void onCreate(Bundle icicle)
78    {
79        if (icicle != null) {
80            mCurrentAlbumId = icicle.getString("selectedalbum");
81            mArtistId = icicle.getString("artist");
82        } else {
83            mArtistId = getIntent().getStringExtra("artist");
84        }
85        super.onCreate(icicle);
86        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
87        setVolumeControlStream(AudioManager.STREAM_MUSIC);
88        MusicUtils.bindToService(this);
89
90        IntentFilter f = new IntentFilter();
91        f.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
92        f.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
93        f.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
94        f.addDataScheme("file");
95        registerReceiver(mScanListener, f);
96
97        setContentView(R.layout.media_picker_activity);
98        ListView lv = getListView();
99        lv.setFastScrollEnabled(true);
100        lv.setOnCreateContextMenuListener(this);
101        lv.setTextFilterEnabled(true);
102
103        mAdapter = (AlbumListAdapter) getLastNonConfigurationInstance();
104        if (mAdapter == null) {
105            //Log.i("@@@", "starting query");
106            mAdapter = new AlbumListAdapter(
107                    getApplication(),
108                    this,
109                    R.layout.track_list_item,
110                    mAlbumCursor,
111                    new String[] {},
112                    new int[] {});
113            setListAdapter(mAdapter);
114            setTitle(R.string.working_albums);
115            getAlbumCursor(mAdapter.getQueryHandler(), null);
116        } else {
117            mAdapter.setActivity(this);
118            setListAdapter(mAdapter);
119            mAlbumCursor = mAdapter.getCursor();
120            if (mAlbumCursor != null) {
121                init(mAlbumCursor);
122            } else {
123                getAlbumCursor(mAdapter.getQueryHandler(), null);
124            }
125        }
126    }
127
128    @Override
129    public Object onRetainNonConfigurationInstance() {
130        mAdapterSent = true;
131        return mAdapter;
132    }
133
134    @Override
135    public void onSaveInstanceState(Bundle outcicle) {
136        // need to store the selected item so we don't lose it in case
137        // of an orientation switch. Otherwise we could lose it while
138        // in the middle of specifying a playlist to add the item to.
139        outcicle.putString("selectedalbum", mCurrentAlbumId);
140        outcicle.putString("artist", mArtistId);
141        super.onSaveInstanceState(outcicle);
142    }
143
144    @Override
145    public void onDestroy() {
146        MusicUtils.unbindFromService(this);
147        if (!mAdapterSent) {
148            Cursor c = mAdapter.getCursor();
149            if (c != null) {
150                c.close();
151            }
152        }
153        // Because we pass the adapter to the next activity, we need to make
154        // sure it doesn't keep a reference to this activity. We can do this
155        // by clearing its DatasetObservers, which setListAdapter(null) does.
156        setListAdapter(null);
157        mAdapter = null;
158        unregisterReceiver(mScanListener);
159        super.onDestroy();
160    }
161
162    @Override
163    public void onResume() {
164        super.onResume();
165        IntentFilter f = new IntentFilter();
166        f.addAction(MediaPlaybackService.META_CHANGED);
167        f.addAction(MediaPlaybackService.QUEUE_CHANGED);
168        registerReceiver(mTrackListListener, f);
169        mTrackListListener.onReceive(null, null);
170
171        MusicUtils.setSpinnerState(this);
172    }
173
174    private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
175        @Override
176        public void onReceive(Context context, Intent intent) {
177            getListView().invalidateViews();
178        }
179    };
180    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
181        @Override
182        public void onReceive(Context context, Intent intent) {
183            MusicUtils.setSpinnerState(AlbumBrowserActivity.this);
184            mReScanHandler.sendEmptyMessage(0);
185            if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
186                MusicUtils.clearAlbumArtCache();
187            }
188        }
189    };
190
191    private Handler mReScanHandler = new Handler() {
192        @Override
193        public void handleMessage(Message msg) {
194            getAlbumCursor(mAdapter.getQueryHandler(), null);
195        }
196    };
197
198    @Override
199    public void onPause() {
200        unregisterReceiver(mTrackListListener);
201        mReScanHandler.removeCallbacksAndMessages(null);
202        super.onPause();
203    }
204
205    public void init(Cursor c) {
206
207        mAdapter.changeCursor(c); // also sets mAlbumCursor
208
209        if (mAlbumCursor == null) {
210            MusicUtils.displayDatabaseError(this);
211            closeContextMenu();
212            mReScanHandler.sendEmptyMessageDelayed(0, 1000);
213            return;
214        }
215
216        MusicUtils.hideDatabaseError(this);
217        setTitle();
218    }
219
220    private void setTitle() {
221        CharSequence fancyName = "";
222        if (mAlbumCursor != null && mAlbumCursor.getCount() > 0) {
223            mAlbumCursor.moveToFirst();
224            fancyName = mAlbumCursor.getString(3);
225            if (fancyName == null || fancyName.equals(MediaFile.UNKNOWN_STRING))
226                fancyName = getText(R.string.unknown_artist_name);
227        }
228
229        if (mArtistId != null && fancyName != null)
230            setTitle(fancyName);
231        else
232            setTitle(R.string.albums_title);
233    }
234
235    @Override
236    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
237        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
238        SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
239        MusicUtils.makePlaylistMenu(this, sub);
240        menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
241        menu.add(0, SEARCH, 0, R.string.search_title);
242
243        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
244        mAlbumCursor.moveToPosition(mi.position);
245        mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
246        mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
247        mCurrentArtistNameForAlbum = mAlbumCursor.getString(
248                mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
249        if (mCurrentAlbumName == null || mCurrentAlbumName.equals(MediaFile.UNKNOWN_STRING)) {
250            menu.setHeaderTitle(getString(R.string.unknown_album_name));
251        } else {
252            menu.setHeaderTitle(mCurrentAlbumName);
253        }
254    }
255
256    @Override
257    public boolean onContextItemSelected(MenuItem item) {
258        switch (item.getItemId()) {
259            case PLAY_SELECTION: {
260                // play the selected album
261                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
262                MusicUtils.playAll(this, list, 0);
263                return true;
264            }
265
266            case QUEUE: {
267                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
268                MusicUtils.addToCurrentPlaylist(this, list);
269                return true;
270            }
271
272            case NEW_PLAYLIST: {
273                Intent intent = new Intent();
274                intent.setClass(this, CreatePlaylist.class);
275                startActivityForResult(intent, NEW_PLAYLIST);
276                return true;
277            }
278
279            case PLAYLIST_SELECTED: {
280                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
281                int playlist = item.getIntent().getIntExtra("playlist", 0);
282                MusicUtils.addToPlaylist(this, list, playlist);
283                return true;
284            }
285            case DELETE_ITEM: {
286                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
287                String f = getString(R.string.delete_album_desc);
288                String desc = String.format(f, mCurrentAlbumName);
289                Bundle b = new Bundle();
290                b.putString("description", desc);
291                b.putIntArray("items", list);
292                Intent intent = new Intent();
293                intent.setClass(this, DeleteItems.class);
294                intent.putExtras(b);
295                startActivityForResult(intent, -1);
296                return true;
297            }
298            case SEARCH:
299                doSearch();
300                return true;
301
302        }
303        return super.onContextItemSelected(item);
304    }
305
306    void doSearch() {
307        CharSequence title = null;
308        String query = null;
309
310        Intent i = new Intent();
311        i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
312        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
313
314        title = mCurrentAlbumName;
315        query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
316        i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
317        i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
318        i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
319        title = getString(R.string.mediasearch, title);
320        i.putExtra(SearchManager.QUERY, query);
321
322        startActivity(Intent.createChooser(i, title));
323    }
324
325    @Override
326    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
327        switch (requestCode) {
328            case SCAN_DONE:
329                if (resultCode == RESULT_CANCELED) {
330                    finish();
331                } else {
332                    getAlbumCursor(mAdapter.getQueryHandler(), null);
333                }
334                break;
335
336            case NEW_PLAYLIST:
337                if (resultCode == RESULT_OK) {
338                    Uri uri = intent.getData();
339                    if (uri != null) {
340                        int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
341                        MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment()));
342                    }
343                }
344                break;
345        }
346    }
347
348    @Override
349    protected void onListItemClick(ListView l, View v, int position, long id)
350    {
351        Intent intent = new Intent(Intent.ACTION_PICK);
352        intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
353        intent.putExtra("album", Long.valueOf(id).toString());
354        intent.putExtra("artist", mArtistId);
355        startActivity(intent);
356    }
357
358    @Override
359    public boolean onCreateOptionsMenu(Menu menu) {
360        super.onCreateOptionsMenu(menu);
361        menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
362        menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback);
363        menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
364        return true;
365    }
366
367    @Override
368    public boolean onPrepareOptionsMenu(Menu menu) {
369        menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded());
370        return super.onPrepareOptionsMenu(menu);
371    }
372
373    @Override
374    public boolean onOptionsItemSelected(MenuItem item) {
375        Intent intent;
376        Cursor cursor;
377        switch (item.getItemId()) {
378            case GOTO_START:
379                intent = new Intent();
380                intent.setClass(this, MusicBrowserActivity.class);
381                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
382                startActivity(intent);
383                return true;
384
385            case GOTO_PLAYBACK:
386                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
387                startActivity(intent);
388                return true;
389
390            case SHUFFLE_ALL:
391                cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
392                        new String [] { MediaStore.Audio.Media._ID},
393                        MediaStore.Audio.Media.IS_MUSIC + "=1", null,
394                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
395                if (cursor != null) {
396                    MusicUtils.shuffleAll(this, cursor);
397                    cursor.close();
398                }
399                return true;
400        }
401        return super.onOptionsItemSelected(item);
402    }
403
404    private Cursor getAlbumCursor(AsyncQueryHandler async, String filter) {
405        StringBuilder where = new StringBuilder();
406        where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
407
408        // Add in the filtering constraints
409        String [] keywords = null;
410        if (filter != null) {
411            String [] searchWords = filter.split(" ");
412            keywords = new String[searchWords.length];
413            Collator col = Collator.getInstance();
414            col.setStrength(Collator.PRIMARY);
415            for (int i = 0; i < searchWords.length; i++) {
416                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
417            }
418            for (int i = 0; i < searchWords.length; i++) {
419                where.append(" AND ");
420                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
421                where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?");
422            }
423        }
424
425        String whereclause = where.toString();
426
427        String[] cols = new String[] {
428                MediaStore.Audio.Albums._ID,
429                MediaStore.Audio.Albums.ARTIST,
430                MediaStore.Audio.Albums.ALBUM,
431                MediaStore.Audio.Albums.ALBUM_ART
432        };
433        Cursor ret = null;
434        if (mArtistId != null) {
435            if (async != null) {
436                async.startQuery(0, null,
437                        MediaStore.Audio.Artists.Albums.getContentUri("external",
438                                Long.valueOf(mArtistId)),
439                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
440            } else {
441                ret = MusicUtils.query(this,
442                        MediaStore.Audio.Artists.Albums.getContentUri("external",
443                                Long.valueOf(mArtistId)),
444                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
445            }
446        } else {
447            if (async != null) {
448                async.startQuery(0, null,
449                        MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
450                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
451            } else {
452                ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
453                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
454            }
455        }
456        return ret;
457    }
458
459    static class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer {
460
461        private final Drawable mNowPlayingOverlay;
462        private final BitmapDrawable mDefaultAlbumIcon;
463        private int mAlbumIdx;
464        private int mArtistIdx;
465        private int mAlbumArtIndex;
466        private final Resources mResources;
467        private final StringBuilder mStringBuilder = new StringBuilder();
468        private final String mUnknownAlbum;
469        private final String mUnknownArtist;
470        private final String mAlbumSongSeparator;
471        private final Object[] mFormatArgs = new Object[1];
472        private AlphabetIndexer mIndexer;
473        private AlbumBrowserActivity mActivity;
474        private AsyncQueryHandler mQueryHandler;
475        private String mConstraint = null;
476        private boolean mConstraintIsValid = false;
477
478        static class ViewHolder {
479            TextView line1;
480            TextView line2;
481            ImageView play_indicator;
482            ImageView icon;
483        }
484
485        class QueryHandler extends AsyncQueryHandler {
486            QueryHandler(ContentResolver res) {
487                super(res);
488            }
489
490            @Override
491            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
492                //Log.i("@@@", "query complete");
493                mActivity.init(cursor);
494            }
495        }
496
497        AlbumListAdapter(Context context, AlbumBrowserActivity currentactivity,
498                int layout, Cursor cursor, String[] from, int[] to) {
499            super(context, layout, cursor, from, to);
500
501            mActivity = currentactivity;
502            mQueryHandler = new QueryHandler(context.getContentResolver());
503
504            mUnknownAlbum = context.getString(R.string.unknown_album_name);
505            mUnknownArtist = context.getString(R.string.unknown_artist_name);
506            mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
507
508            Resources r = context.getResources();
509            mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
510
511            Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list);
512            mDefaultAlbumIcon = new BitmapDrawable(b);
513            // no filter or dither, it's a lot faster and we can't tell the difference
514            mDefaultAlbumIcon.setFilterBitmap(false);
515            mDefaultAlbumIcon.setDither(false);
516            getColumnIndices(cursor);
517            mResources = context.getResources();
518        }
519
520        private void getColumnIndices(Cursor cursor) {
521            if (cursor != null) {
522                mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
523                mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST);
524                mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART);
525
526                if (mIndexer != null) {
527                    mIndexer.setCursor(cursor);
528                } else {
529                    mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString(
530                            com.android.internal.R.string.fast_scroll_alphabet));
531                }
532            }
533        }
534
535        public void setActivity(AlbumBrowserActivity newactivity) {
536            mActivity = newactivity;
537        }
538
539        public AsyncQueryHandler getQueryHandler() {
540            return mQueryHandler;
541        }
542
543        @Override
544        public View newView(Context context, Cursor cursor, ViewGroup parent) {
545           View v = super.newView(context, cursor, parent);
546           ViewHolder vh = new ViewHolder();
547           vh.line1 = (TextView) v.findViewById(R.id.line1);
548           vh.line2 = (TextView) v.findViewById(R.id.line2);
549           vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
550           vh.icon = (ImageView) v.findViewById(R.id.icon);
551           vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
552           vh.icon.setPadding(0, 0, 1, 0);
553           v.setTag(vh);
554           return v;
555        }
556
557        @Override
558        public void bindView(View view, Context context, Cursor cursor) {
559
560            ViewHolder vh = (ViewHolder) view.getTag();
561
562            String name = cursor.getString(mAlbumIdx);
563            String displayname = name;
564            boolean unknown = name == null || name.equals(MediaFile.UNKNOWN_STRING);
565            if (unknown) {
566                displayname = mUnknownAlbum;
567            }
568            vh.line1.setText(displayname);
569
570            name = cursor.getString(mArtistIdx);
571            displayname = name;
572            if (name == null || name.equals(MediaFile.UNKNOWN_STRING)) {
573                displayname = mUnknownArtist;
574            }
575            vh.line2.setText(displayname);
576
577            ImageView iv = vh.icon;
578            // We don't actually need the path to the thumbnail file,
579            // we just use it to see if there is album art or not
580            String art = cursor.getString(mAlbumArtIndex);
581            if (unknown || art == null || art.length() == 0) {
582                iv.setImageDrawable(null);
583            } else {
584                int artIndex = cursor.getInt(0);
585                Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon);
586                iv.setImageDrawable(d);
587            }
588
589            int currentalbumid = MusicUtils.getCurrentAlbumId();
590            int aid = cursor.getInt(0);
591            iv = vh.play_indicator;
592            if (currentalbumid == aid) {
593                iv.setImageDrawable(mNowPlayingOverlay);
594            } else {
595                iv.setImageDrawable(null);
596            }
597        }
598
599        @Override
600        public void changeCursor(Cursor cursor) {
601            if (cursor != mActivity.mAlbumCursor) {
602                mActivity.mAlbumCursor = cursor;
603                getColumnIndices(cursor);
604                super.changeCursor(cursor);
605            }
606        }
607
608        @Override
609        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
610            String s = constraint.toString();
611            if (mConstraintIsValid && (
612                    (s == null && mConstraint == null) ||
613                    (s != null && s.equals(mConstraint)))) {
614                return getCursor();
615            }
616            Cursor c = mActivity.getAlbumCursor(null, s);
617            mConstraint = s;
618            mConstraintIsValid = true;
619            return c;
620        }
621
622        public Object[] getSections() {
623            return mIndexer.getSections();
624        }
625
626        public int getPositionForSection(int section) {
627            return mIndexer.getPositionForSection(section);
628        }
629
630        public int getSectionForPosition(int position) {
631            return 0;
632        }
633    }
634
635    private Cursor mAlbumCursor;
636    private String mArtistId;
637}
638
639