MusicPicker.java revision 95c42939a307b02e84fbcd4186974a7607657996
1/*
2 * Copyright (C) 2008 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.content.AsyncQueryHandler;
21import android.content.ContentUris;
22import android.content.Context;
23import android.content.Intent;
24import android.database.CharArrayBuffer;
25import android.database.Cursor;
26import android.media.AudioManager;
27import android.media.MediaPlayer;
28import android.media.RingtoneManager;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Parcelable;
32import android.provider.MediaStore;
33import android.util.Log;
34import android.view.Menu;
35import android.view.MenuItem;
36import android.view.View;
37import android.view.ViewGroup;
38import android.view.Window;
39import android.view.animation.AnimationUtils;
40import android.widget.ImageView;
41import android.widget.ListView;
42import android.widget.RadioButton;
43import android.widget.SectionIndexer;
44import android.widget.SimpleCursorAdapter;
45import android.widget.TextView;
46
47import java.io.IOException;
48import java.text.Collator;
49import java.util.Formatter;
50import java.util.Locale;
51
52/**
53 * Activity allowing the user to select a music track on the device, and
54 * return it to its caller.  The music picker user interface is fairly
55 * extensive, providing information about each track like the music
56 * application (title, author, album, duration), as well as the ability to
57 * previous tracks and sort them in different orders.
58 *
59 * <p>This class also illustrates how you can load data from a content
60 * provider asynchronously, providing a good UI while doing so, perform
61 * indexing of the content for use inside of a {@link FastScrollView}, and
62 * perform filtering of the data as the user presses keys.
63 */
64public class MusicPicker extends ListActivity
65        implements View.OnClickListener, MediaPlayer.OnCompletionListener,
66        MusicUtils.Defs {
67    static final boolean DBG = false;
68    static final String TAG = "MusicPicker";
69
70    /** Holds the previous state of the list, to restore after the async
71     * query has completed. */
72    static final String LIST_STATE_KEY = "liststate";
73    /** Remember whether the list last had focus for restoring its state. */
74    static final String FOCUS_KEY = "focused";
75    /** Remember the last ordering mode for restoring state. */
76    static final String SORT_MODE_KEY = "sortMode";
77
78    /** Arbitrary number, doesn't matter since we only do one query type. */
79    final int MY_QUERY_TOKEN = 42;
80
81    /** Menu item to sort the music list by track title. */
82    static final int TRACK_MENU = Menu.FIRST;
83    /** Menu item to sort the music list by album title. */
84    static final int ALBUM_MENU = Menu.FIRST+1;
85    /** Menu item to sort the music list by artist name. */
86    static final int ARTIST_MENU = Menu.FIRST+2;
87
88    /** These are the columns in the music cursor that we are interested in. */
89    static final String[] CURSOR_COLS = new String[] {
90            MediaStore.Audio.Media._ID,
91            MediaStore.Audio.Media.TITLE,
92            MediaStore.Audio.Media.TITLE_KEY,
93            MediaStore.Audio.Media.DATA,
94            MediaStore.Audio.Media.ALBUM,
95            MediaStore.Audio.Media.ARTIST,
96            MediaStore.Audio.Media.ARTIST_ID,
97            MediaStore.Audio.Media.DURATION,
98            MediaStore.Audio.Media.TRACK
99    };
100
101    /** Formatting optimization to avoid creating many temporary objects. */
102    static StringBuilder sFormatBuilder = new StringBuilder();
103    /** Formatting optimization to avoid creating many temporary objects. */
104    static Formatter sFormatter = new Formatter(sFormatBuilder, Locale.getDefault());
105    /** Formatting optimization to avoid creating many temporary objects. */
106    static final Object[] sTimeArgs = new Object[5];
107
108    /** Uri to the directory of all music being displayed. */
109    Uri mBaseUri;
110
111    /** This is the adapter used to display all of the tracks. */
112    TrackListAdapter mAdapter;
113    /** Our instance of QueryHandler used to perform async background queries. */
114    QueryHandler mQueryHandler;
115
116    /** Used to keep track of the last scroll state of the list. */
117    Parcelable mListState = null;
118    /** Used to keep track of whether the list last had focus. */
119    boolean mListHasFocus;
120
121    /** The current cursor on the music that is being displayed. */
122    Cursor mCursor;
123    /** The actual sort order the user has selected. */
124    int mSortMode = -1;
125    /** SQL order by string describing the currently selected sort order. */
126    String mSortOrder;
127
128    /** Container of the in-screen progress indicator, to be able to hide it
129     * when done loading the initial cursor. */
130    View mProgressContainer;
131    /** Container of the list view hierarchy, to be able to show it when done
132     * loading the initial cursor. */
133    View mListContainer;
134    /** Set to true when the list view has been shown for the first time. */
135    boolean mListShown;
136
137    /** View holding the okay button. */
138    View mOkayButton;
139    /** View holding the cancel button. */
140    View mCancelButton;
141
142    /** Which track row ID the user has last selected. */
143    long mSelectedId = -1;
144    /** Completel Uri that the user has last selected. */
145    Uri mSelectedUri;
146
147    /** If >= 0, we are currently playing a track for preview, and this is its
148     * row ID. */
149    long mPlayingId = -1;
150
151    /** This is used for playing previews of the music files. */
152    MediaPlayer mMediaPlayer;
153
154    /**
155     * A special implementation of SimpleCursorAdapter that knows how to bind
156     * our cursor data to our list item structure, and takes care of other
157     * advanced features such as indexing and filtering.
158     */
159    class TrackListAdapter extends SimpleCursorAdapter
160            implements SectionIndexer {
161        final ListView mListView;
162
163        private final StringBuilder mBuilder = new StringBuilder();
164        private final String mUnknownArtist;
165        private final String mUnknownAlbum;
166
167        private int mIdIdx;
168        private int mTitleIdx;
169        private int mArtistIdx;
170        private int mAlbumIdx;
171        private int mDurationIdx;
172        private int mAudioIdIdx;
173        private int mTrackIdx;
174
175        private boolean mLoading = true;
176        private int mIndexerSortMode;
177        private boolean mIndexerOutOfDate;
178        private MusicAlphabetIndexer mIndexer;
179
180        class ViewHolder {
181            TextView line1;
182            TextView line2;
183            TextView duration;
184            RadioButton radio;
185            ImageView play_indicator;
186            CharArrayBuffer buffer1;
187            char [] buffer2;
188        }
189
190        TrackListAdapter(Context context, ListView listView, int layout,
191                String[] from, int[] to) {
192            super(context, layout, null, from, to);
193            mListView = listView;
194            mUnknownArtist = context.getString(R.string.unknown_artist_name);
195            mUnknownAlbum = context.getString(R.string.unknown_album_name);
196        }
197
198        /**
199         * The mLoading flag is set while we are performing a background
200         * query, to avoid displaying the "No music" empty view during
201         * this time.
202         */
203        public void setLoading(boolean loading) {
204            mLoading = loading;
205        }
206
207        @Override
208        public boolean isEmpty() {
209            if (mLoading) {
210                // We don't want the empty state to show when loading.
211                return false;
212            } else {
213                return super.isEmpty();
214            }
215        }
216
217        @Override
218        public View newView(Context context, Cursor cursor, ViewGroup parent) {
219            View v = super.newView(context, cursor, parent);
220            ViewHolder vh = new ViewHolder();
221            vh.line1 = (TextView) v.findViewById(R.id.line1);
222            vh.line2 = (TextView) v.findViewById(R.id.line2);
223            vh.duration = (TextView) v.findViewById(R.id.duration);
224            vh.radio = (RadioButton) v.findViewById(R.id.radio);
225            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
226            vh.buffer1 = new CharArrayBuffer(100);
227            vh.buffer2 = new char[200];
228            v.setTag(vh);
229            return v;
230        }
231
232        @Override
233        public void bindView(View view, Context context, Cursor cursor) {
234            ViewHolder vh = (ViewHolder) view.getTag();
235
236            cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
237            vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
238
239            int secs = cursor.getInt(mDurationIdx) / 1000;
240            if (secs == 0) {
241                vh.duration.setText("");
242            } else {
243                vh.duration.setText(makeTimeString(context, secs));
244            }
245
246            final StringBuilder builder = mBuilder;
247            builder.delete(0, builder.length());
248
249            String name = cursor.getString(mAlbumIdx);
250            if (name == null || name.equals("<unknown>")) {
251                builder.append(mUnknownAlbum);
252            } else {
253                builder.append(name);
254            }
255            builder.append('\n');
256            name = cursor.getString(mArtistIdx);
257            if (name == null || name.equals("<unknown>")) {
258                builder.append(mUnknownArtist);
259            } else {
260                builder.append(name);
261            }
262            int len = builder.length();
263            if (vh.buffer2.length < len) {
264                vh.buffer2 = new char[len];
265            }
266            builder.getChars(0, len, vh.buffer2, 0);
267            vh.line2.setText(vh.buffer2, 0, len);
268
269            // Update the checkbox of the item, based on which the user last
270            // selected.  Note that doing it this way means we must have the
271            // list view update all of its items when the selected item
272            // changes.
273            final long id = cursor.getLong(mIdIdx);
274            vh.radio.setChecked(id == mSelectedId);
275            if (DBG) Log.v(TAG, "Binding id=" + id + " sel=" + mSelectedId
276                    + " playing=" + mPlayingId + " cursor=" + cursor);
277
278            // Likewise, display the "now playing" icon if this item is
279            // currently being previewed for the user.
280            ImageView iv = vh.play_indicator;
281            if (id == mPlayingId) {
282                iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
283                iv.setVisibility(View.VISIBLE);
284            } else {
285                iv.setVisibility(View.GONE);
286            }
287        }
288
289        /**
290         * This method is called whenever we receive a new cursor due to
291         * an async query, and must take care of plugging the new one in
292         * to the adapter.
293         */
294        @Override
295        public void changeCursor(Cursor cursor) {
296            super.changeCursor(cursor);
297            if (DBG) Log.v(TAG, "Setting cursor to: " + cursor
298                    + " from: " + MusicPicker.this.mCursor);
299
300            MusicPicker.this.mCursor = cursor;
301
302            if (cursor != null) {
303                // Retrieve indices of the various columns we are interested in.
304                mIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
305                mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
306                mArtistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
307                mAlbumIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
308                mDurationIdx = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
309                int audioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID);
310                if (audioIdIdx < 0) {
311                    audioIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
312                }
313                mAudioIdIdx = audioIdIdx;
314                mTrackIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TRACK);
315            }
316
317            // The next time the indexer is needed, we will need to rebind it
318            // to this cursor.
319            mIndexerOutOfDate = true;
320
321            // Ensure that the list is shown (and initial progress indicator
322            // hidden) in case this is the first cursor we have gotten.
323            makeListShown();
324        }
325
326        /**
327         * This method is called from a background thread by the list view
328         * when the user has typed a letter that should result in a filtering
329         * of the displayed items.  It returns a Cursor, when will then be
330         * handed to changeCursor.
331         */
332        @Override
333        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
334            if (DBG) Log.v(TAG, "Getting new cursor...");
335            return doQuery(true, constraint.toString());
336        }
337
338        public int getPositionForSection(int section) {
339            Cursor cursor = getCursor();
340            if (cursor == null) {
341                // No cursor, the section doesn't exist so just return 0
342                return 0;
343            }
344
345            // If the sort mode has changed, or we haven't yet created an
346            // indexer one, then create a new one that is indexing the
347            // appropriate column based on the sort mode.
348            if (mIndexerSortMode != mSortMode || mIndexer == null) {
349                mIndexerSortMode = mSortMode;
350                int idx = mTitleIdx;
351                switch (mIndexerSortMode) {
352                    case ARTIST_MENU:
353                        idx = mArtistIdx;
354                        break;
355                    case ALBUM_MENU:
356                        idx = mAlbumIdx;
357                        break;
358                }
359                mIndexer = new MusicAlphabetIndexer(cursor, idx,
360                        getResources().getString(
361                                com.android.internal.R.string.fast_scroll_alphabet));
362
363            // If we have a valid indexer, but the cursor has changed since
364            // its last use, then point it to the current cursor.
365            } else if (mIndexerOutOfDate) {
366                mIndexer.setCursor(cursor);
367            }
368
369            mIndexerOutOfDate = false;
370
371            return mIndexer.getPositionForSection(section);
372        }
373
374        public int getSectionForPosition(int position) {
375            return 0;
376        }
377
378        public Object[] getSections() {
379            return mIndexer.getSections();
380        }
381    }
382
383    /**
384     * This is our specialization of AsyncQueryHandler applies new cursors
385     * to our state as they become available.
386     */
387    private final class QueryHandler extends AsyncQueryHandler {
388        public QueryHandler(Context context) {
389            super(context.getContentResolver());
390        }
391
392        @Override
393        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
394            if (!isFinishing()) {
395                // Update the adapter: we are no longer loading, and have
396                // a new cursor for it.
397                mAdapter.setLoading(false);
398                mAdapter.changeCursor(cursor);
399                setProgressBarIndeterminateVisibility(false);
400
401                // Now that the cursor is populated again, it's possible to restore the list state
402                if (mListState != null) {
403                    getListView().onRestoreInstanceState(mListState);
404                    if (mListHasFocus) {
405                        getListView().requestFocus();
406                    }
407                    mListHasFocus = false;
408                    mListState = null;
409                }
410            } else {
411                cursor.close();
412            }
413        }
414    }
415
416    public static String makeTimeString(Context context, long secs) {
417        String durationformat = context.getString(R.string.durationformat);
418
419        /* Provide multiple arguments so the format can be changed easily
420         * by modifying the xml.
421         */
422        sFormatBuilder.setLength(0);
423
424        final Object[] timeArgs = sTimeArgs;
425        timeArgs[0] = secs / 3600;
426        timeArgs[1] = secs / 60;
427        timeArgs[2] = (secs / 60) % 60;
428        timeArgs[3] = secs;
429        timeArgs[4] = secs % 60;
430
431        return sFormatter.format(durationformat, timeArgs).toString();
432    }
433
434    /** Called when the activity is first created. */
435    @Override
436    public void onCreate(Bundle icicle) {
437        super.onCreate(icicle);
438
439        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
440
441        int sortMode = TRACK_MENU;
442        if (icicle == null) {
443            mSelectedUri = getIntent().getParcelableExtra(
444                    RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
445        } else {
446            mSelectedUri = (Uri)icicle.getParcelable(
447                    RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
448            // Retrieve list state. This will be applied after the
449            // QueryHandler has run
450            mListState = icicle.getParcelable(LIST_STATE_KEY);
451            mListHasFocus = icicle.getBoolean(FOCUS_KEY);
452            sortMode = icicle.getInt(SORT_MODE_KEY, sortMode);
453        }
454        if (Intent.ACTION_GET_CONTENT.equals(getIntent().getAction())) {
455            mBaseUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
456        } else {
457            mBaseUri = getIntent().getData();
458            if (mBaseUri == null) {
459                Log.w("MusicPicker", "No data URI given to PICK action");
460                finish();
461                return;
462            }
463        }
464
465        setContentView(R.layout.music_picker);
466
467        mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
468
469        final ListView listView = getListView();
470
471        listView.setItemsCanFocus(false);
472
473        mAdapter = new TrackListAdapter(this, listView,
474                R.layout.music_picker_item, new String[] {},
475                new int[] {});
476
477        setListAdapter(mAdapter);
478
479        listView.setTextFilterEnabled(true);
480
481        // We manually save/restore the listview state
482        listView.setSaveEnabled(false);
483
484        mQueryHandler = new QueryHandler(this);
485
486        mProgressContainer = findViewById(R.id.progressContainer);
487        mListContainer = findViewById(R.id.listContainer);
488
489        mOkayButton = findViewById(R.id.okayButton);
490        mOkayButton.setOnClickListener(this);
491        mCancelButton = findViewById(R.id.cancelButton);
492        mCancelButton.setOnClickListener(this);
493
494        // If there is a currently selected Uri, then try to determine who
495        // it is.
496        if (mSelectedUri != null) {
497            Uri.Builder builder = mSelectedUri.buildUpon();
498            String path = mSelectedUri.getEncodedPath();
499            int idx = path.lastIndexOf('/');
500            if (idx >= 0) {
501                path = path.substring(0, idx);
502            }
503            builder.encodedPath(path);
504            Uri baseSelectedUri = builder.build();
505            if (DBG) Log.v(TAG, "Selected Uri: " + mSelectedUri);
506            if (DBG) Log.v(TAG, "Selected base Uri: " + baseSelectedUri);
507            if (DBG) Log.v(TAG, "Base Uri: " + mBaseUri);
508            if (baseSelectedUri.equals(mBaseUri)) {
509                // If the base Uri of the selected Uri is the same as our
510                // content's base Uri, then use the selection!
511                mSelectedId = ContentUris.parseId(mSelectedUri);
512            }
513        }
514
515        setSortMode(sortMode);
516    }
517
518    @Override public void onRestart() {
519        super.onRestart();
520        doQuery(false, null);
521    }
522
523    @Override public boolean onOptionsItemSelected(MenuItem item) {
524        if (setSortMode(item.getItemId())) {
525            return true;
526        }
527        return super.onOptionsItemSelected(item);
528    }
529
530    @Override public boolean onCreateOptionsMenu(Menu menu) {
531        super.onCreateOptionsMenu(menu);
532        menu.add(Menu.NONE, TRACK_MENU, Menu.NONE, R.string.sort_by_track);
533        menu.add(Menu.NONE, ALBUM_MENU, Menu.NONE, R.string.sort_by_album);
534        menu.add(Menu.NONE, ARTIST_MENU, Menu.NONE, R.string.sort_by_artist);
535        return true;
536    }
537
538    @Override protected void onSaveInstanceState(Bundle icicle) {
539        super.onSaveInstanceState(icicle);
540        // Save list state in the bundle so we can restore it after the
541        // QueryHandler has run
542        icicle.putParcelable(LIST_STATE_KEY, getListView().onSaveInstanceState());
543        icicle.putBoolean(FOCUS_KEY, getListView().hasFocus());
544        icicle.putInt(SORT_MODE_KEY, mSortMode);
545    }
546
547    @Override public void onPause() {
548        super.onPause();
549        stopMediaPlayer();
550    }
551
552    @Override public void onStop() {
553        super.onStop();
554
555        // We don't want the list to display the empty state, since when we
556        // resume it will still be there and show up while the new query is
557        // happening. After the async query finishes in response to onResume()
558        // setLoading(false) will be called.
559        mAdapter.setLoading(true);
560        mAdapter.changeCursor(null);
561    }
562
563    /**
564     * Changes the current sort order, building the appropriate query string
565     * for the selected order.
566     */
567    boolean setSortMode(int sortMode) {
568        if (sortMode != mSortMode) {
569            switch (sortMode) {
570                case TRACK_MENU:
571                    mSortMode = sortMode;
572                    mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
573                    doQuery(false, null);
574                    return true;
575                case ALBUM_MENU:
576                    mSortMode = sortMode;
577                    mSortOrder = MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
578                            + MediaStore.Audio.Media.TRACK + " ASC, "
579                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
580                    doQuery(false, null);
581                    return true;
582                case ARTIST_MENU:
583                    mSortMode = sortMode;
584                    mSortOrder = MediaStore.Audio.Media.ARTIST_KEY + " ASC, "
585                            + MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
586                            + MediaStore.Audio.Media.TRACK + " ASC, "
587                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
588                    doQuery(false, null);
589                    return true;
590            }
591
592        }
593        return false;
594    }
595
596    /**
597     * The first time this is called, we hide the large progress indicator
598     * and show the list view, doing fade animations between them.
599     */
600    void makeListShown() {
601        if (!mListShown) {
602            mListShown = true;
603            mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
604                    this, android.R.anim.fade_out));
605            mProgressContainer.setVisibility(View.GONE);
606            mListContainer.startAnimation(AnimationUtils.loadAnimation(
607                    this, android.R.anim.fade_in));
608            mListContainer.setVisibility(View.VISIBLE);
609        }
610    }
611
612    /**
613     * Common method for performing a query of the music database, called for
614     * both top-level queries and filtering.
615     *
616     * @param sync If true, this query should be done synchronously and the
617     * resulting cursor returned.  If false, it will be done asynchronously and
618     * null returned.
619     * @param filterstring If non-null, this is a filter to apply to the query.
620     */
621    Cursor doQuery(boolean sync, String filterstring) {
622        // Cancel any pending queries
623        mQueryHandler.cancelOperation(MY_QUERY_TOKEN);
624
625        StringBuilder where = new StringBuilder();
626        where.append(MediaStore.Audio.Media.TITLE + " != ''");
627
628        // Add in the filtering constraints
629        String [] keywords = null;
630        if (filterstring != null) {
631            String [] searchWords = filterstring.split(" ");
632            keywords = new String[searchWords.length];
633            Collator col = Collator.getInstance();
634            col.setStrength(Collator.PRIMARY);
635            for (int i = 0; i < searchWords.length; i++) {
636                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
637            }
638            for (int i = 0; i < searchWords.length; i++) {
639                where.append(" AND ");
640                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
641                where.append(MediaStore.Audio.Media.ALBUM_KEY + "||");
642                where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?");
643            }
644        }
645
646        // We want to show all audio files, even recordings.  Enforcing the
647        // following condition would hide recordings.
648        //where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
649
650        if (sync) {
651            try {
652                return getContentResolver().query(mBaseUri, CURSOR_COLS,
653                        where.toString(), keywords, mSortOrder);
654            } catch (UnsupportedOperationException ex) {
655            }
656        } else {
657            mAdapter.setLoading(true);
658            setProgressBarIndeterminateVisibility(true);
659            mQueryHandler.startQuery(MY_QUERY_TOKEN, null, mBaseUri, CURSOR_COLS,
660                    where.toString(), keywords, mSortOrder);
661        }
662        return null;
663    }
664
665    @Override protected void onListItemClick(ListView l, View v, int position,
666            long id) {
667        mCursor.moveToPosition(position);
668        if (DBG) Log.v(TAG, "Click on " + position + " (id=" + id
669                + ", cursid="
670                + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID))
671                + ") in cursor " + mCursor
672                + " adapter=" + l.getAdapter());
673        setSelected(mCursor);
674    }
675
676    void setSelected(Cursor c) {
677        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
678        long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
679        mSelectedUri = ContentUris.withAppendedId(uri, newId);
680
681        mSelectedId = newId;
682        if (newId != mPlayingId || mMediaPlayer == null) {
683            stopMediaPlayer();
684            mMediaPlayer = new MediaPlayer();
685            try {
686                mMediaPlayer.setDataSource(this, mSelectedUri);
687                mMediaPlayer.setOnCompletionListener(this);
688                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
689                mMediaPlayer.prepare();
690                mMediaPlayer.start();
691                mPlayingId = newId;
692                getListView().invalidateViews();
693            } catch (IOException e) {
694                Log.w("MusicPicker", "Unable to play track", e);
695            }
696        } else if (mMediaPlayer != null) {
697            stopMediaPlayer();
698            getListView().invalidateViews();
699        }
700    }
701
702    public void onCompletion(MediaPlayer mp) {
703        if (mMediaPlayer == mp) {
704            mp.stop();
705            mp.release();
706            mMediaPlayer = null;
707            mPlayingId = -1;
708            getListView().invalidateViews();
709        }
710    }
711
712    void stopMediaPlayer() {
713        if (mMediaPlayer != null) {
714            mMediaPlayer.stop();
715            mMediaPlayer.release();
716            mMediaPlayer = null;
717            mPlayingId = -1;
718        }
719    }
720
721    public void onClick(View v) {
722        switch (v.getId()) {
723            case R.id.okayButton:
724                if (mSelectedId >= 0) {
725                    setResult(RESULT_OK, new Intent().setData(mSelectedUri));
726                    finish();
727                }
728                break;
729
730            case R.id.cancelButton:
731                finish();
732                break;
733        }
734    }
735}
736