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