• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util.Log;
34 import android.view.Menu;
35 import android.view.MenuItem;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.Window;
39 import android.view.animation.AnimationUtils;
40 import android.widget.ImageView;
41 import android.widget.ListView;
42 import android.widget.RadioButton;
43 import android.widget.SectionIndexer;
44 import android.widget.SimpleCursorAdapter;
45 import android.widget.TextView;
46 
47 import java.io.IOException;
48 import java.text.Collator;
49 import java.util.Formatter;
50 import 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  */
64 public 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 
TrackListAdapter(Context context, ListView listView, int layout, String[] from, int[] to)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          */
setLoading(boolean loading)200         public void setLoading(boolean loading) {
201             mLoading = loading;
202         }
203 
204         @Override
isEmpty()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
newView(Context context, Cursor cursor, ViewGroup parent)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
bindView(View view, Context context, Cursor cursor)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(MusicUtils.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
changeCursor(Cursor cursor)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
runQueryOnBackgroundThread(CharSequence constraint)344         public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
345             if (DBG) Log.v(TAG, "Getting new cursor...");
346             return doQuery(true, constraint.toString());
347         }
348 
getPositionForSection(int section)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 
getSectionForPosition(int position)359         public int getSectionForPosition(int position) {
360             return 0;
361         }
362 
getSections()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 {
QueryHandler(Context context)376         public QueryHandler(Context context) {
377             super(context.getContentResolver());
378         }
379 
380         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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
onCreate(Bundle icicle)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 
onRestart()488     @Override public void onRestart() {
489         super.onRestart();
490         doQuery(false, null);
491     }
492 
onOptionsItemSelected(MenuItem item)493     @Override public boolean onOptionsItemSelected(MenuItem item) {
494         if (setSortMode(item.getItemId())) {
495             return true;
496         }
497         return super.onOptionsItemSelected(item);
498     }
499 
onCreateOptionsMenu(Menu menu)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 
onSaveInstanceState(Bundle icicle)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 
onPause()517     @Override public void onPause() {
518         super.onPause();
519         stopMediaPlayer();
520     }
521 
onStop()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      */
setSortMode(int sortMode)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      */
makeListShown()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      */
doQuery(boolean sync, String filterstring)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         // Add in the filtering constraints
599         String [] keywords = null;
600         if (filterstring != null) {
601             String [] searchWords = filterstring.split(" ");
602             keywords = new String[searchWords.length];
603             Collator col = Collator.getInstance();
604             col.setStrength(Collator.PRIMARY);
605             for (int i = 0; i < searchWords.length; i++) {
606                 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%';
607             }
608             for (int i = 0; i < searchWords.length; i++) {
609                 where.append(" AND ");
610                 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||");
611                 where.append(MediaStore.Audio.Media.ALBUM_KEY + "||");
612                 where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?");
613             }
614         }
615 
616         // We want to show all audio files, even recordings.  Enforcing the
617         // following condition would hide recordings.
618         //where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1");
619 
620         if (sync) {
621             try {
622                 return getContentResolver().query(mBaseUri, CURSOR_COLS,
623                         where.toString(), keywords, mSortOrder);
624             } catch (UnsupportedOperationException ex) {
625             }
626         } else {
627             mAdapter.setLoading(true);
628             setProgressBarIndeterminateVisibility(true);
629             mQueryHandler.startQuery(MY_QUERY_TOKEN, null, mBaseUri, CURSOR_COLS,
630                     where.toString(), keywords, mSortOrder);
631         }
632         return null;
633     }
634 
onListItemClick(ListView l, View v, int position, long id)635     @Override protected void onListItemClick(ListView l, View v, int position,
636             long id) {
637         mCursor.moveToPosition(position);
638         if (DBG) Log.v(TAG, "Click on " + position + " (id=" + id
639                 + ", cursid="
640                 + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID))
641                 + ") in cursor " + mCursor
642                 + " adapter=" + l.getAdapter());
643         setSelected(mCursor);
644     }
645 
setSelected(Cursor c)646     void setSelected(Cursor c) {
647         Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
648         long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID));
649         mSelectedUri = ContentUris.withAppendedId(uri, newId);
650 
651         mSelectedId = newId;
652         if (newId != mPlayingId || mMediaPlayer == null) {
653             stopMediaPlayer();
654             mMediaPlayer = new MediaPlayer();
655             try {
656                 mMediaPlayer.setDataSource(this, mSelectedUri);
657                 mMediaPlayer.setOnCompletionListener(this);
658                 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
659                 mMediaPlayer.prepare();
660                 mMediaPlayer.start();
661                 mPlayingId = newId;
662                 getListView().invalidateViews();
663             } catch (IOException e) {
664                 Log.w("MusicPicker", "Unable to play track", e);
665             }
666         } else if (mMediaPlayer != null) {
667             stopMediaPlayer();
668             getListView().invalidateViews();
669         }
670     }
671 
onCompletion(MediaPlayer mp)672     public void onCompletion(MediaPlayer mp) {
673         if (mMediaPlayer == mp) {
674             mp.stop();
675             mp.release();
676             mMediaPlayer = null;
677             mPlayingId = -1;
678             getListView().invalidateViews();
679         }
680     }
681 
stopMediaPlayer()682     void stopMediaPlayer() {
683         if (mMediaPlayer != null) {
684             mMediaPlayer.stop();
685             mMediaPlayer.release();
686             mMediaPlayer = null;
687             mPlayingId = -1;
688         }
689     }
690 
onClick(View v)691     public void onClick(View v) {
692         switch (v.getId()) {
693             case R.id.okayButton:
694                 if (mSelectedId >= 0) {
695                     setResult(RESULT_OK, new Intent().setData(mSelectedUri));
696                     finish();
697                 }
698                 break;
699 
700             case R.id.cancelButton:
701                 finish();
702                 break;
703         }
704     }
705 }
706