MusicPicker.java revision 4c7fcfad7fd8000d6cc4d99026e895d06b6b5624
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    static 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
173        private boolean mLoading = true;
174        private int mIndexerSortMode;
175        private MusicAlphabetIndexer mIndexer;
176
177        class ViewHolder {
178            TextView line1;
179            TextView line2;
180            TextView duration;
181            RadioButton radio;
182            ImageView play_indicator;
183            CharArrayBuffer buffer1;
184            char [] buffer2;
185        }
186
187        TrackListAdapter(Context context, ListView listView, int layout,
188                String[] from, int[] to) {
189            super(context, layout, null, from, to);
190            mListView = listView;
191            mUnknownArtist = context.getString(R.string.unknown_artist_name);
192            mUnknownAlbum = context.getString(R.string.unknown_album_name);
193        }
194
195        /**
196         * The mLoading flag is set while we are performing a background
197         * query, to avoid displaying the "No music" empty view during
198         * this time.
199         */
200        public void setLoading(boolean loading) {
201            mLoading = loading;
202        }
203
204        @Override
205        public boolean isEmpty() {
206            if (mLoading) {
207                // We don't want the empty state to show when loading.
208                return false;
209            } else {
210                return super.isEmpty();
211            }
212        }
213
214        @Override
215        public View newView(Context context, Cursor cursor, ViewGroup parent) {
216            View v = super.newView(context, cursor, parent);
217            ViewHolder vh = new ViewHolder();
218            vh.line1 = (TextView) v.findViewById(R.id.line1);
219            vh.line2 = (TextView) v.findViewById(R.id.line2);
220            vh.duration = (TextView) v.findViewById(R.id.duration);
221            vh.radio = (RadioButton) v.findViewById(R.id.radio);
222            vh.play_indicator = (ImageView) v.findViewById(R.id.play_indicator);
223            vh.buffer1 = new CharArrayBuffer(100);
224            vh.buffer2 = new char[200];
225            v.setTag(vh);
226            return v;
227        }
228
229        @Override
230        public void bindView(View view, Context context, Cursor cursor) {
231            ViewHolder vh = (ViewHolder) view.getTag();
232
233            cursor.copyStringToBuffer(mTitleIdx, vh.buffer1);
234            vh.line1.setText(vh.buffer1.data, 0, vh.buffer1.sizeCopied);
235
236            int secs = cursor.getInt(mDurationIdx) / 1000;
237            if (secs == 0) {
238                vh.duration.setText("");
239            } else {
240                vh.duration.setText(makeTimeString(context, secs));
241            }
242
243            final StringBuilder builder = mBuilder;
244            builder.delete(0, builder.length());
245
246            String name = cursor.getString(mAlbumIdx);
247            if (name == null || name.equals("<unknown>")) {
248                builder.append(mUnknownAlbum);
249            } else {
250                builder.append(name);
251            }
252            builder.append('\n');
253            name = cursor.getString(mArtistIdx);
254            if (name == null || name.equals("<unknown>")) {
255                builder.append(mUnknownArtist);
256            } else {
257                builder.append(name);
258            }
259            int len = builder.length();
260            if (vh.buffer2.length < len) {
261                vh.buffer2 = new char[len];
262            }
263            builder.getChars(0, len, vh.buffer2, 0);
264            vh.line2.setText(vh.buffer2, 0, len);
265
266            // Update the checkbox of the item, based on which the user last
267            // selected.  Note that doing it this way means we must have the
268            // list view update all of its items when the selected item
269            // changes.
270            final long id = cursor.getLong(mIdIdx);
271            vh.radio.setChecked(id == mSelectedId);
272            if (DBG) Log.v(TAG, "Binding id=" + id + " sel=" + mSelectedId
273                    + " playing=" + mPlayingId + " cursor=" + cursor);
274
275            // Likewise, display the "now playing" icon if this item is
276            // currently being previewed for the user.
277            ImageView iv = vh.play_indicator;
278            if (id == mPlayingId) {
279                iv.setImageResource(R.drawable.indicator_ic_mp_playing_list);
280                iv.setVisibility(View.VISIBLE);
281            } else {
282                iv.setVisibility(View.GONE);
283            }
284        }
285
286        /**
287         * This method is called whenever we receive a new cursor due to
288         * an async query, and must take care of plugging the new one in
289         * to the adapter.
290         */
291        @Override
292        public void changeCursor(Cursor cursor) {
293            super.changeCursor(cursor);
294            if (DBG) Log.v(TAG, "Setting cursor to: " + cursor
295                    + " from: " + MusicPicker.this.mCursor);
296
297            MusicPicker.this.mCursor = cursor;
298
299            if (cursor != null) {
300                // Retrieve indices of the various columns we are interested in.
301                mIdIdx = cursor.getColumnIndex(MediaStore.Audio.Media._ID);
302                mTitleIdx = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
303                mArtistIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
304                mAlbumIdx = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM);
305                mDurationIdx = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
306
307                // If the sort mode has changed, or we haven't yet created an
308                // indexer one, then create a new one that is indexing the
309                // appropriate column based on the sort mode.
310                if (mIndexerSortMode != mSortMode || mIndexer == null) {
311                    mIndexerSortMode = mSortMode;
312                    int idx = mTitleIdx;
313                    switch (mIndexerSortMode) {
314                        case ARTIST_MENU:
315                            idx = mArtistIdx;
316                            break;
317                        case ALBUM_MENU:
318                            idx = mAlbumIdx;
319                            break;
320                    }
321                    mIndexer = new MusicAlphabetIndexer(cursor, idx,
322                            getResources().getString(
323                                    com.android.internal.R.string.fast_scroll_alphabet));
324
325                // If we have a valid indexer, but the cursor has changed since
326                // its last use, then point it to the current cursor.
327                } else {
328                    mIndexer.setCursor(cursor);
329                }
330            }
331
332            // Ensure that the list is shown (and initial progress indicator
333            // hidden) in case this is the first cursor we have gotten.
334            makeListShown();
335        }
336
337        /**
338         * This method is called from a background thread by the list view
339         * when the user has typed a letter that should result in a filtering
340         * of the displayed items.  It returns a Cursor, when will then be
341         * handed to changeCursor.
342         */
343        @Override
344        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
345            if (DBG) Log.v(TAG, "Getting new cursor...");
346            return doQuery(true, constraint.toString());
347        }
348
349        public int getPositionForSection(int section) {
350            Cursor cursor = getCursor();
351            if (cursor == null) {
352                // No cursor, the section doesn't exist so just return 0
353                return 0;
354            }
355
356            return mIndexer.getPositionForSection(section);
357        }
358
359        public int getSectionForPosition(int position) {
360            return 0;
361        }
362
363        public Object[] getSections() {
364            if (mIndexer != null) {
365                return mIndexer.getSections();
366            }
367            return null;
368        }
369    }
370
371    /**
372     * This is our specialization of AsyncQueryHandler applies new cursors
373     * to our state as they become available.
374     */
375    private final class QueryHandler extends AsyncQueryHandler {
376        public QueryHandler(Context context) {
377            super(context.getContentResolver());
378        }
379
380        @Override
381        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
382            if (!isFinishing()) {
383                // Update the adapter: we are no longer loading, and have
384                // a new cursor for it.
385                mAdapter.setLoading(false);
386                mAdapter.changeCursor(cursor);
387                setProgressBarIndeterminateVisibility(false);
388
389                // Now that the cursor is populated again, it's possible to restore the list state
390                if (mListState != null) {
391                    getListView().onRestoreInstanceState(mListState);
392                    if (mListHasFocus) {
393                        getListView().requestFocus();
394                    }
395                    mListHasFocus = false;
396                    mListState = null;
397                }
398            } else {
399                cursor.close();
400            }
401        }
402    }
403
404    public static String makeTimeString(Context context, long secs) {
405        String durationformat = context.getString(R.string.durationformat);
406
407        /* Provide multiple arguments so the format can be changed easily
408         * by modifying the xml.
409         */
410        sFormatBuilder.setLength(0);
411
412        final Object[] timeArgs = sTimeArgs;
413        timeArgs[0] = secs / 3600;
414        timeArgs[1] = secs / 60;
415        timeArgs[2] = (secs / 60) % 60;
416        timeArgs[3] = secs;
417        timeArgs[4] = secs % 60;
418
419        return sFormatter.format(durationformat, timeArgs).toString();
420    }
421
422    /** Called when the activity is first created. */
423    @Override
424    public void onCreate(Bundle icicle) {
425        super.onCreate(icicle);
426
427        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
428
429        int sortMode = TRACK_MENU;
430        if (icicle == null) {
431            mSelectedUri = getIntent().getParcelableExtra(
432                    RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
433        } else {
434            mSelectedUri = (Uri)icicle.getParcelable(
435                    RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
436            // Retrieve list state. This will be applied after the
437            // QueryHandler has run
438            mListState = icicle.getParcelable(LIST_STATE_KEY);
439            mListHasFocus = icicle.getBoolean(FOCUS_KEY);
440            sortMode = icicle.getInt(SORT_MODE_KEY, sortMode);
441        }
442        if (Intent.ACTION_GET_CONTENT.equals(getIntent().getAction())) {
443            mBaseUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
444        } else {
445            mBaseUri = getIntent().getData();
446            if (mBaseUri == null) {
447                Log.w("MusicPicker", "No data URI given to PICK action");
448                finish();
449                return;
450            }
451        }
452
453        setContentView(R.layout.music_picker);
454
455        mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
456
457        final ListView listView = getListView();
458
459        listView.setItemsCanFocus(false);
460
461        mAdapter = new TrackListAdapter(this, listView,
462                R.layout.music_picker_item, new String[] {},
463                new int[] {});
464
465        setListAdapter(mAdapter);
466
467        listView.setTextFilterEnabled(true);
468
469        // We manually save/restore the listview state
470        listView.setSaveEnabled(false);
471
472        mQueryHandler = new QueryHandler(this);
473
474        mProgressContainer = findViewById(R.id.progressContainer);
475        mListContainer = findViewById(R.id.listContainer);
476
477        mOkayButton = findViewById(R.id.okayButton);
478        mOkayButton.setOnClickListener(this);
479        mCancelButton = findViewById(R.id.cancelButton);
480        mCancelButton.setOnClickListener(this);
481
482        // If there is a currently selected Uri, then try to determine who
483        // it is.
484        if (mSelectedUri != null) {
485            Uri.Builder builder = mSelectedUri.buildUpon();
486            String path = mSelectedUri.getEncodedPath();
487            int idx = path.lastIndexOf('/');
488            if (idx >= 0) {
489                path = path.substring(0, idx);
490            }
491            builder.encodedPath(path);
492            Uri baseSelectedUri = builder.build();
493            if (DBG) Log.v(TAG, "Selected Uri: " + mSelectedUri);
494            if (DBG) Log.v(TAG, "Selected base Uri: " + baseSelectedUri);
495            if (DBG) Log.v(TAG, "Base Uri: " + mBaseUri);
496            if (baseSelectedUri.equals(mBaseUri)) {
497                // If the base Uri of the selected Uri is the same as our
498                // content's base Uri, then use the selection!
499                mSelectedId = ContentUris.parseId(mSelectedUri);
500            }
501        }
502
503        setSortMode(sortMode);
504    }
505
506    @Override public void onRestart() {
507        super.onRestart();
508        doQuery(false, null);
509    }
510
511    @Override public boolean onOptionsItemSelected(MenuItem item) {
512        if (setSortMode(item.getItemId())) {
513            return true;
514        }
515        return super.onOptionsItemSelected(item);
516    }
517
518    @Override public boolean onCreateOptionsMenu(Menu menu) {
519        super.onCreateOptionsMenu(menu);
520        menu.add(Menu.NONE, TRACK_MENU, Menu.NONE, R.string.sort_by_track);
521        menu.add(Menu.NONE, ALBUM_MENU, Menu.NONE, R.string.sort_by_album);
522        menu.add(Menu.NONE, ARTIST_MENU, Menu.NONE, R.string.sort_by_artist);
523        return true;
524    }
525
526    @Override protected void onSaveInstanceState(Bundle icicle) {
527        super.onSaveInstanceState(icicle);
528        // Save list state in the bundle so we can restore it after the
529        // QueryHandler has run
530        icicle.putParcelable(LIST_STATE_KEY, getListView().onSaveInstanceState());
531        icicle.putBoolean(FOCUS_KEY, getListView().hasFocus());
532        icicle.putInt(SORT_MODE_KEY, mSortMode);
533    }
534
535    @Override public void onPause() {
536        super.onPause();
537        stopMediaPlayer();
538    }
539
540    @Override public void onStop() {
541        super.onStop();
542
543        // We don't want the list to display the empty state, since when we
544        // resume it will still be there and show up while the new query is
545        // happening. After the async query finishes in response to onResume()
546        // setLoading(false) will be called.
547        mAdapter.setLoading(true);
548        mAdapter.changeCursor(null);
549    }
550
551    /**
552     * Changes the current sort order, building the appropriate query string
553     * for the selected order.
554     */
555    boolean setSortMode(int sortMode) {
556        if (sortMode != mSortMode) {
557            switch (sortMode) {
558                case TRACK_MENU:
559                    mSortMode = sortMode;
560                    mSortOrder = MediaStore.Audio.Media.TITLE_KEY;
561                    doQuery(false, null);
562                    return true;
563                case ALBUM_MENU:
564                    mSortMode = sortMode;
565                    mSortOrder = MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
566                            + MediaStore.Audio.Media.TRACK + " ASC, "
567                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
568                    doQuery(false, null);
569                    return true;
570                case ARTIST_MENU:
571                    mSortMode = sortMode;
572                    mSortOrder = MediaStore.Audio.Media.ARTIST_KEY + " ASC, "
573                            + MediaStore.Audio.Media.ALBUM_KEY + " ASC, "
574                            + MediaStore.Audio.Media.TRACK + " ASC, "
575                            + MediaStore.Audio.Media.TITLE_KEY + " ASC";
576                    doQuery(false, null);
577                    return true;
578            }
579
580        }
581        return false;
582    }
583
584    /**
585     * The first time this is called, we hide the large progress indicator
586     * and show the list view, doing fade animations between them.
587     */
588    void makeListShown() {
589        if (!mListShown) {
590            mListShown = true;
591            mProgressContainer.startAnimation(AnimationUtils.loadAnimation(
592                    this, android.R.anim.fade_out));
593            mProgressContainer.setVisibility(View.GONE);
594            mListContainer.startAnimation(AnimationUtils.loadAnimation(
595                    this, android.R.anim.fade_in));
596            mListContainer.setVisibility(View.VISIBLE);
597        }
598    }
599
600    /**
601     * Common method for performing a query of the music database, called for
602     * both top-level queries and filtering.
603     *
604     * @param sync If true, this query should be done synchronously and the
605     * resulting cursor returned.  If false, it will be done asynchronously and
606     * null returned.
607     * @param filterstring If non-null, this is a filter to apply to the query.
608     */
609    Cursor doQuery(boolean sync, String filterstring) {
610        // Cancel any pending queries
611        mQueryHandler.cancelOperation(MY_QUERY_TOKEN);
612
613        StringBuilder where = new StringBuilder();
614        where.append(MediaStore.Audio.Media.TITLE + " != ''");
615
616        // Add in the filtering constraints
617        String [] keywords = null;
618        if (filterstring != null) {
619            String [] searchWords = filterstring.split(" ");
620            keywords = new String[searchWords.length];
621            Collator col = Collator.getInstance();
622            col.setStrength(Collator.PRIMARY);
623            for (int i = 0; i < searchWords.length; i++) {
624                keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
625            }
626            for (int i = 0; i < searchWords.length; i++) {
627                where.append(" AND ");
628                where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
629                where.append(MediaStore.Audio.Media.ALBUM_KEY + "||");
630                where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?");
631            }
632        }
633
634        // We want to show all audio files, even recordings.  Enforcing the
635        // following condition would hide recordings.
636        //where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
637
638        if (sync) {
639            try {
640                return getContentResolver().query(mBaseUri, CURSOR_COLS,
641                        where.toString(), keywords, mSortOrder);
642            } catch (UnsupportedOperationException ex) {
643            }
644        } else {
645            mAdapter.setLoading(true);
646            setProgressBarIndeterminateVisibility(true);
647            mQueryHandler.startQuery(MY_QUERY_TOKEN, null, mBaseUri, CURSOR_COLS,
648                    where.toString(), keywords, mSortOrder);
649        }
650        return null;
651    }
652
653    @Override protected void onListItemClick(ListView l, View v, int position,
654            long id) {
655        mCursor.moveToPosition(position);
656        if (DBG) Log.v(TAG, "Click on " + position + " (id=" + id
657                + ", cursid="
658                + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID))
659                + ") in cursor " + mCursor
660                + " adapter=" + l.getAdapter());
661        setSelected(mCursor);
662    }
663
664    void setSelected(Cursor c) {
665        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
666        long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
667        mSelectedUri = ContentUris.withAppendedId(uri, newId);
668
669        mSelectedId = newId;
670        if (newId != mPlayingId || mMediaPlayer == null) {
671            stopMediaPlayer();
672            mMediaPlayer = new MediaPlayer();
673            try {
674                mMediaPlayer.setDataSource(this, mSelectedUri);
675                mMediaPlayer.setOnCompletionListener(this);
676                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
677                mMediaPlayer.prepare();
678                mMediaPlayer.start();
679                mPlayingId = newId;
680                getListView().invalidateViews();
681            } catch (IOException e) {
682                Log.w("MusicPicker", "Unable to play track", e);
683            }
684        } else if (mMediaPlayer != null) {
685            stopMediaPlayer();
686            getListView().invalidateViews();
687        }
688    }
689
690    public void onCompletion(MediaPlayer mp) {
691        if (mMediaPlayer == mp) {
692            mp.stop();
693            mp.release();
694            mMediaPlayer = null;
695            mPlayingId = -1;
696            getListView().invalidateViews();
697        }
698    }
699
700    void stopMediaPlayer() {
701        if (mMediaPlayer != null) {
702            mMediaPlayer.stop();
703            mMediaPlayer.release();
704            mMediaPlayer = null;
705            mPlayingId = -1;
706        }
707    }
708
709    public void onClick(View v) {
710        switch (v.getId()) {
711            case R.id.okayButton:
712                if (mSelectedId >= 0) {
713                    setResult(RESULT_OK, new Intent().setData(mSelectedUri));
714                    finish();
715                }
716                break;
717
718            case R.id.cancelButton:
719                finish();
720                break;
721        }
722    }
723}
724