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