AlbumBrowserActivity.java revision 539844af4c430cf8d1f138d1482520bc4cd3847d
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        unregisterReceiver(mScanListener);
154        super.onDestroy();
155    }
156
157    @Override
158    public void onResume() {
159        super.onResume();
160        IntentFilter f = new IntentFilter();
161        f.addAction(MediaPlaybackService.META_CHANGED);
162        f.addAction(MediaPlaybackService.QUEUE_CHANGED);
163        registerReceiver(mTrackListListener, f);
164        mTrackListListener.onReceive(null, null);
165
166        MusicUtils.setSpinnerState(this);
167    }
168
169    private BroadcastReceiver mTrackListListener = new BroadcastReceiver() {
170        @Override
171        public void onReceive(Context context, Intent intent) {
172            getListView().invalidateViews();
173        }
174    };
175    private BroadcastReceiver mScanListener = new BroadcastReceiver() {
176        @Override
177        public void onReceive(Context context, Intent intent) {
178            MusicUtils.setSpinnerState(AlbumBrowserActivity.this);
179            mReScanHandler.sendEmptyMessage(0);
180            if (intent.getAction().equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
181                MusicUtils.clearAlbumArtCache();
182            }
183        }
184    };
185
186    private Handler mReScanHandler = new Handler() {
187        @Override
188        public void handleMessage(Message msg) {
189            getAlbumCursor(mAdapter.getQueryHandler(), null);
190        }
191    };
192
193    @Override
194    public void onPause() {
195        unregisterReceiver(mTrackListListener);
196        mReScanHandler.removeCallbacksAndMessages(null);
197        super.onPause();
198    }
199
200    public void init(Cursor c) {
201
202        mAdapter.changeCursor(c); // also sets mAlbumCursor
203
204        if (mAlbumCursor == null) {
205            MusicUtils.displayDatabaseError(this);
206            closeContextMenu();
207            mReScanHandler.sendEmptyMessageDelayed(0, 1000);
208            return;
209        }
210
211        MusicUtils.hideDatabaseError(this);
212        setTitle();
213    }
214
215    private void setTitle() {
216        CharSequence fancyName = "";
217        if (mAlbumCursor != null && mAlbumCursor.getCount() > 0) {
218            mAlbumCursor.moveToFirst();
219            fancyName = mAlbumCursor.getString(3);
220            if (MediaFile.UNKNOWN_STRING.equals(fancyName))
221                fancyName = getText(R.string.unknown_artist_name);
222        }
223
224        if (mArtistId != null && fancyName != null)
225            setTitle(fancyName);
226        else
227            setTitle(R.string.albums_title);
228    }
229
230    @Override
231    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfoIn) {
232        menu.add(0, PLAY_SELECTION, 0, R.string.play_selection);
233        SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, R.string.add_to_playlist);
234        MusicUtils.makePlaylistMenu(this, sub);
235        menu.add(0, DELETE_ITEM, 0, R.string.delete_item);
236        menu.add(0, SEARCH, 0, R.string.search_title);
237
238        AdapterContextMenuInfo mi = (AdapterContextMenuInfo) menuInfoIn;
239        mAlbumCursor.moveToPosition(mi.position);
240        mCurrentAlbumId = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums._ID));
241        mCurrentAlbumName = mAlbumCursor.getString(mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM));
242        mCurrentArtistNameForAlbum = mAlbumCursor.getString(
243                mAlbumCursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST));
244        menu.setHeaderTitle(mCurrentAlbumName);
245    }
246
247    @Override
248    public boolean onContextItemSelected(MenuItem item) {
249        switch (item.getItemId()) {
250            case PLAY_SELECTION: {
251                // play the selected album
252                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
253                MusicUtils.playAll(this, list, 0);
254                return true;
255            }
256
257            case QUEUE: {
258                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
259                MusicUtils.addToCurrentPlaylist(this, list);
260                return true;
261            }
262
263            case NEW_PLAYLIST: {
264                Intent intent = new Intent();
265                intent.setClass(this, CreatePlaylist.class);
266                startActivityForResult(intent, NEW_PLAYLIST);
267                return true;
268            }
269
270            case PLAYLIST_SELECTED: {
271                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
272                int playlist = item.getIntent().getIntExtra("playlist", 0);
273                MusicUtils.addToPlaylist(this, list, playlist);
274                return true;
275            }
276            case DELETE_ITEM: {
277                int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
278                String f = getString(R.string.delete_album_desc);
279                String desc = String.format(f, mCurrentAlbumName);
280                Bundle b = new Bundle();
281                b.putString("description", desc);
282                b.putIntArray("items", list);
283                Intent intent = new Intent();
284                intent.setClass(this, DeleteItems.class);
285                intent.putExtras(b);
286                startActivityForResult(intent, -1);
287                return true;
288            }
289            case SEARCH:
290                doSearch();
291                return true;
292
293        }
294        return super.onContextItemSelected(item);
295    }
296
297    void doSearch() {
298        CharSequence title = null;
299        String query = null;
300
301        Intent i = new Intent();
302        i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH);
303
304        title = mCurrentAlbumName;
305        query = mCurrentArtistNameForAlbum + " " + mCurrentAlbumName;
306        i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, mCurrentArtistNameForAlbum);
307        i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, mCurrentAlbumName);
308        i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE);
309        title = getString(R.string.mediasearch, title);
310        i.putExtra(SearchManager.QUERY, query);
311
312        startActivity(Intent.createChooser(i, title));
313    }
314
315    @Override
316    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
317        switch (requestCode) {
318            case SCAN_DONE:
319                if (resultCode == RESULT_CANCELED) {
320                    finish();
321                } else {
322                    getAlbumCursor(mAdapter.getQueryHandler(), null);
323                }
324                break;
325
326            case NEW_PLAYLIST:
327                if (resultCode == RESULT_OK) {
328                    Uri uri = intent.getData();
329                    if (uri != null) {
330                        int [] list = MusicUtils.getSongListForAlbum(this, Integer.parseInt(mCurrentAlbumId));
331                        MusicUtils.addToPlaylist(this, list, Integer.parseInt(uri.getLastPathSegment()));
332                    }
333                }
334                break;
335        }
336    }
337
338    @Override
339    protected void onListItemClick(ListView l, View v, int position, long id)
340    {
341        if (mHasHeader) {
342            position --;
343        }
344        Intent intent = new Intent(Intent.ACTION_PICK);
345        intent.setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track");
346        intent.putExtra("album", Long.valueOf(id).toString());
347        intent.putExtra("artist", mArtistId);
348        startActivity(intent);
349    }
350
351    @Override
352    public boolean onCreateOptionsMenu(Menu menu) {
353        super.onCreateOptionsMenu(menu);
354        menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library);
355        menu.add(0, GOTO_PLAYBACK, 0, R.string.goto_playback).setIcon(R.drawable.ic_menu_playback);
356        menu.add(0, SHUFFLE_ALL, 0, R.string.shuffle_all).setIcon(R.drawable.ic_menu_shuffle);
357        return true;
358    }
359
360    @Override
361    public boolean onPrepareOptionsMenu(Menu menu) {
362        menu.findItem(GOTO_PLAYBACK).setVisible(MusicUtils.isMusicLoaded());
363        return super.onPrepareOptionsMenu(menu);
364    }
365
366    @Override
367    public boolean onOptionsItemSelected(MenuItem item) {
368        Intent intent;
369        Cursor cursor;
370        switch (item.getItemId()) {
371            case GOTO_START:
372                intent = new Intent();
373                intent.setClass(this, MusicBrowserActivity.class);
374                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
375                startActivity(intent);
376                return true;
377
378            case GOTO_PLAYBACK:
379                intent = new Intent("com.android.music.PLAYBACK_VIEWER");
380                startActivity(intent);
381                return true;
382
383            case SHUFFLE_ALL:
384                cursor = MusicUtils.query(this, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
385                        new String [] { MediaStore.Audio.Media._ID},
386                        MediaStore.Audio.Media.IS_MUSIC + "=1", null,
387                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
388                if (cursor != null) {
389                    MusicUtils.shuffleAll(this, cursor);
390                    cursor.close();
391                }
392                return true;
393        }
394        return super.onOptionsItemSelected(item);
395    }
396
397    private Cursor getAlbumCursor(AsyncQueryHandler async, String filter) {
398        StringBuilder where = new StringBuilder();
399        where.append(MediaStore.Audio.Albums.ALBUM + " != ''");
400
401        // Add in the filtering constraints
402        String [] keywords = null;
403        if (filter != null) {
404            String [] searchWords = filter.split(" ");
405            keywords = new String[searchWords.length];
406            Collator col = Collator.getInstance();
407            col.setStrength(Collator.PRIMARY);
408            for (int i = 0; i < searchWords.length; i++) {
409                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
410            }
411            for (int i = 0; i < searchWords.length; i++) {
412                where.append(" AND ");
413                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
414                where.append(MediaStore.Audio.Media.ALBUM_KEY + " LIKE ?");
415            }
416        }
417
418        String whereclause = where.toString();
419
420        String[] cols = new String[] {
421                MediaStore.Audio.Albums._ID,
422                MediaStore.Audio.Albums.ALBUM,
423                MediaStore.Audio.Albums.ALBUM_KEY,
424                MediaStore.Audio.Albums.ARTIST,
425                MediaStore.Audio.Albums.NUMBER_OF_SONGS,
426                MediaStore.Audio.Albums.ALBUM_ART
427        };
428        Cursor ret = null;
429        if (mArtistId != null) {
430            if (async != null) {
431                async.startQuery(0, null,
432                        MediaStore.Audio.Artists.Albums.getContentUri("external",
433                                Long.valueOf(mArtistId)),
434                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
435            } else {
436                ret = MusicUtils.query(this,
437                        MediaStore.Audio.Artists.Albums.getContentUri("external",
438                                Long.valueOf(mArtistId)),
439                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
440            }
441        } else {
442            if (async != null) {
443                async.startQuery(0, null,
444                        MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
445                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
446            } else {
447                ret = MusicUtils.query(this, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
448                        cols, whereclause, keywords, MediaStore.Audio.Albums.DEFAULT_SORT_ORDER);
449            }
450        }
451        return ret;
452    }
453
454    static class AlbumListAdapter extends SimpleCursorAdapter implements SectionIndexer {
455
456        private final Drawable mNowPlayingOverlay;
457        private final BitmapDrawable mDefaultAlbumIcon;
458        private int mAlbumIdx;
459        private int mArtistIdx;
460        private int mNumSongsIdx;
461        private int mAlbumArtIndex;
462        private final Resources mResources;
463        private final StringBuilder mStringBuilder = new StringBuilder();
464        private final String mUnknownAlbum;
465        private final String mUnknownArtist;
466        private final String mAlbumSongSeparator;
467        private final Object[] mFormatArgs = new Object[1];
468        private AlphabetIndexer mIndexer;
469        private AlbumBrowserActivity mActivity;
470        private AsyncQueryHandler mQueryHandler;
471
472        class ViewHolder {
473            TextView line1;
474            TextView line2;
475            TextView duration;
476            ImageView play_indicator;
477            ImageView icon;
478        }
479
480        class QueryHandler extends AsyncQueryHandler {
481            QueryHandler(ContentResolver res) {
482                super(res);
483            }
484
485            @Override
486            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
487                //Log.i("@@@", "query complete");
488                mActivity.init(cursor);
489            }
490        }
491
492        AlbumListAdapter(Context context, AlbumBrowserActivity currentactivity,
493                int layout, Cursor cursor, String[] from, int[] to) {
494            super(context, layout, cursor, from, to);
495
496            mActivity = currentactivity;
497            mQueryHandler = new QueryHandler(context.getContentResolver());
498
499            mUnknownAlbum = context.getString(R.string.unknown_album_name);
500            mUnknownArtist = context.getString(R.string.unknown_artist_name);
501            mAlbumSongSeparator = context.getString(R.string.albumsongseparator);
502
503            Resources r = context.getResources();
504            mNowPlayingOverlay = r.getDrawable(R.drawable.indicator_ic_mp_playing_list);
505
506            Bitmap b = BitmapFactory.decodeResource(r, R.drawable.albumart_mp_unknown_list);
507            mDefaultAlbumIcon = new BitmapDrawable(b);
508            // no filter or dither, it's a lot faster and we can't tell the difference
509            mDefaultAlbumIcon.setFilterBitmap(false);
510            mDefaultAlbumIcon.setDither(false);
511            getColumnIndices(cursor);
512            mResources = context.getResources();
513        }
514
515        private void getColumnIndices(Cursor cursor) {
516            if (cursor != null) {
517                mAlbumIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM);
518                mArtistIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST);
519                mNumSongsIdx = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS);
520                mAlbumArtIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM_ART);
521
522                if (mIndexer != null) {
523                    mIndexer.setCursor(cursor);
524                } else {
525                    mIndexer = new MusicAlphabetIndexer(cursor, mAlbumIdx, mResources.getString(
526                            com.android.internal.R.string.fast_scroll_alphabet));
527                }
528            }
529        }
530
531        public void setActivity(AlbumBrowserActivity newactivity) {
532            mActivity = newactivity;
533        }
534
535        public AsyncQueryHandler getQueryHandler() {
536            return mQueryHandler;
537        }
538
539        @Override
540        public View newView(Context context, Cursor cursor, ViewGroup parent) {
541           View v = super.newView(context, cursor, parent);
542           ViewHolder vh = new ViewHolder();
543           vh.line1 = (TextView) v.findViewById(R.id.line1);
544           vh.line2 = (TextView) v.findViewById(R.id.line2);
545           vh.duration = (TextView) v.findViewById(R.id.duration);
546           vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
547           vh.icon = (ImageView) v.findViewById(R.id.icon);
548           vh.icon.setBackgroundDrawable(mDefaultAlbumIcon);
549           vh.icon.setPadding(0, 0, 1, 0);
550           v.setTag(vh);
551           return v;
552        }
553
554        @Override
555        public void bindView(View view, Context context, Cursor cursor) {
556
557            ViewHolder vh = (ViewHolder) view.getTag();
558
559            String name = cursor.getString(mAlbumIdx);
560            String displayname = name;
561            boolean unknown = name.equals(MediaFile.UNKNOWN_STRING);
562            if (unknown) {
563                displayname = mUnknownAlbum;
564            }
565            vh.line1.setText(displayname);
566
567            name = cursor.getString(mArtistIdx);
568            displayname = name;
569            if (MediaFile.UNKNOWN_STRING.equals(name)) {
570                displayname = mUnknownArtist;
571            }
572            vh.line2.setText(displayname);
573
574            ImageView iv = vh.icon;
575            // We don't actually need the path to the thumbnail file,
576            // we just use it to see if there is album art or not
577            String art = cursor.getString(mAlbumArtIndex);
578            if (unknown || art == null || art.length() == 0) {
579                iv.setImageDrawable(null);
580            } else {
581                int artIndex = cursor.getInt(0);
582                Drawable d = MusicUtils.getCachedArtwork(context, artIndex, mDefaultAlbumIcon);
583                iv.setImageDrawable(d);
584            }
585
586            int currentalbumid = MusicUtils.getCurrentAlbumId();
587            int aid = cursor.getInt(0);
588            iv = vh.play_indicator;
589            if (currentalbumid == aid) {
590                iv.setImageDrawable(mNowPlayingOverlay);
591            } else {
592                iv.setImageDrawable(null);
593            }
594        }
595
596        @Override
597        public void changeCursor(Cursor cursor) {
598            if (cursor != mActivity.mAlbumCursor) {
599                mActivity.mAlbumCursor = cursor;
600                getColumnIndices(cursor);
601                super.changeCursor(cursor);
602            }
603        }
604
605        @Override
606        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
607            return mActivity.getAlbumCursor(null, constraint.toString());
608        }
609
610        public Object[] getSections() {
611            return mIndexer.getSections();
612        }
613
614        public int getPositionForSection(int section) {
615            return mIndexer.getPositionForSection(section);
616        }
617
618        public int getSectionForPosition(int position) {
619            return 0;
620        }
621    }
622
623    private Cursor mAlbumCursor;
624    private String mArtistId;
625    private boolean mHasHeader = false;
626}
627
628