MusicPicker.java revision 756c3f57f11a7bb3bf1cf1333ee8ffbc51039748
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 boolean mIndexerOutOfDate; 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(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 309 // The next time the indexer is needed, we will need to rebind it 310 // to this cursor. 311 mIndexerOutOfDate = true; 312 313 // Ensure that the list is shown (and initial progress indicator 314 // hidden) in case this is the first cursor we have gotten. 315 makeListShown(); 316 } 317 318 /** 319 * This method is called from a background thread by the list view 320 * when the user has typed a letter that should result in a filtering 321 * of the displayed items. It returns a Cursor, when will then be 322 * handed to changeCursor. 323 */ 324 @Override 325 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 326 if (DBG) Log.v(TAG, "Getting new cursor..."); 327 return doQuery(true, constraint.toString()); 328 } 329 330 public int getPositionForSection(int section) { 331 Cursor cursor = getCursor(); 332 if (cursor == null) { 333 // No cursor, the section doesn't exist so just return 0 334 return 0; 335 } 336 337 // If the sort mode has changed, or we haven't yet created an 338 // indexer one, then create a new one that is indexing the 339 // appropriate column based on the sort mode. 340 if (mIndexerSortMode != mSortMode || mIndexer == null) { 341 mIndexerSortMode = mSortMode; 342 int idx = mTitleIdx; 343 switch (mIndexerSortMode) { 344 case ARTIST_MENU: 345 idx = mArtistIdx; 346 break; 347 case ALBUM_MENU: 348 idx = mAlbumIdx; 349 break; 350 } 351 mIndexer = new MusicAlphabetIndexer(cursor, idx, 352 getResources().getString( 353 com.android.internal.R.string.fast_scroll_alphabet)); 354 355 // If we have a valid indexer, but the cursor has changed since 356 // its last use, then point it to the current cursor. 357 } else if (mIndexerOutOfDate) { 358 mIndexer.setCursor(cursor); 359 } 360 361 mIndexerOutOfDate = false; 362 363 return mIndexer.getPositionForSection(section); 364 } 365 366 public int getSectionForPosition(int position) { 367 return 0; 368 } 369 370 public Object[] getSections() { 371 return mIndexer.getSections(); 372 } 373 } 374 375 /** 376 * This is our specialization of AsyncQueryHandler applies new cursors 377 * to our state as they become available. 378 */ 379 private final class QueryHandler extends AsyncQueryHandler { 380 public QueryHandler(Context context) { 381 super(context.getContentResolver()); 382 } 383 384 @Override 385 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 386 if (!isFinishing()) { 387 // Update the adapter: we are no longer loading, and have 388 // a new cursor for it. 389 mAdapter.setLoading(false); 390 mAdapter.changeCursor(cursor); 391 setProgressBarIndeterminateVisibility(false); 392 393 // Now that the cursor is populated again, it's possible to restore the list state 394 if (mListState != null) { 395 getListView().onRestoreInstanceState(mListState); 396 if (mListHasFocus) { 397 getListView().requestFocus(); 398 } 399 mListHasFocus = false; 400 mListState = null; 401 } 402 } else { 403 cursor.close(); 404 } 405 } 406 } 407 408 public static String makeTimeString(Context context, long secs) { 409 String durationformat = context.getString(R.string.durationformat); 410 411 /* Provide multiple arguments so the format can be changed easily 412 * by modifying the xml. 413 */ 414 sFormatBuilder.setLength(0); 415 416 final Object[] timeArgs = sTimeArgs; 417 timeArgs[0] = secs / 3600; 418 timeArgs[1] = secs / 60; 419 timeArgs[2] = (secs / 60) % 60; 420 timeArgs[3] = secs; 421 timeArgs[4] = secs % 60; 422 423 return sFormatter.format(durationformat, timeArgs).toString(); 424 } 425 426 /** Called when the activity is first created. */ 427 @Override 428 public void onCreate(Bundle icicle) { 429 super.onCreate(icicle); 430 431 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); 432 433 int sortMode = TRACK_MENU; 434 if (icicle == null) { 435 mSelectedUri = getIntent().getParcelableExtra( 436 RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); 437 } else { 438 mSelectedUri = (Uri)icicle.getParcelable( 439 RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); 440 // Retrieve list state. This will be applied after the 441 // QueryHandler has run 442 mListState = icicle.getParcelable(LIST_STATE_KEY); 443 mListHasFocus = icicle.getBoolean(FOCUS_KEY); 444 sortMode = icicle.getInt(SORT_MODE_KEY, sortMode); 445 } 446 if (Intent.ACTION_GET_CONTENT.equals(getIntent().getAction())) { 447 mBaseUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 448 } else { 449 mBaseUri = getIntent().getData(); 450 if (mBaseUri == null) { 451 Log.w("MusicPicker", "No data URI given to PICK action"); 452 finish(); 453 return; 454 } 455 } 456 457 setContentView(R.layout.music_picker); 458 459 mSortOrder = MediaStore.Audio.Media.TITLE_KEY; 460 461 final ListView listView = getListView(); 462 463 listView.setItemsCanFocus(false); 464 465 mAdapter = new TrackListAdapter(this, listView, 466 R.layout.music_picker_item, new String[] {}, 467 new int[] {}); 468 469 setListAdapter(mAdapter); 470 471 listView.setTextFilterEnabled(true); 472 473 // We manually save/restore the listview state 474 listView.setSaveEnabled(false); 475 476 mQueryHandler = new QueryHandler(this); 477 478 mProgressContainer = findViewById(R.id.progressContainer); 479 mListContainer = findViewById(R.id.listContainer); 480 481 mOkayButton = findViewById(R.id.okayButton); 482 mOkayButton.setOnClickListener(this); 483 mCancelButton = findViewById(R.id.cancelButton); 484 mCancelButton.setOnClickListener(this); 485 486 // If there is a currently selected Uri, then try to determine who 487 // it is. 488 if (mSelectedUri != null) { 489 Uri.Builder builder = mSelectedUri.buildUpon(); 490 String path = mSelectedUri.getEncodedPath(); 491 int idx = path.lastIndexOf('/'); 492 if (idx >= 0) { 493 path = path.substring(0, idx); 494 } 495 builder.encodedPath(path); 496 Uri baseSelectedUri = builder.build(); 497 if (DBG) Log.v(TAG, "Selected Uri: " + mSelectedUri); 498 if (DBG) Log.v(TAG, "Selected base Uri: " + baseSelectedUri); 499 if (DBG) Log.v(TAG, "Base Uri: " + mBaseUri); 500 if (baseSelectedUri.equals(mBaseUri)) { 501 // If the base Uri of the selected Uri is the same as our 502 // content's base Uri, then use the selection! 503 mSelectedId = ContentUris.parseId(mSelectedUri); 504 } 505 } 506 507 setSortMode(sortMode); 508 } 509 510 @Override public void onRestart() { 511 super.onRestart(); 512 doQuery(false, null); 513 } 514 515 @Override public boolean onOptionsItemSelected(MenuItem item) { 516 if (setSortMode(item.getItemId())) { 517 return true; 518 } 519 return super.onOptionsItemSelected(item); 520 } 521 522 @Override public boolean onCreateOptionsMenu(Menu menu) { 523 super.onCreateOptionsMenu(menu); 524 menu.add(Menu.NONE, TRACK_MENU, Menu.NONE, R.string.sort_by_track); 525 menu.add(Menu.NONE, ALBUM_MENU, Menu.NONE, R.string.sort_by_album); 526 menu.add(Menu.NONE, ARTIST_MENU, Menu.NONE, R.string.sort_by_artist); 527 return true; 528 } 529 530 @Override protected void onSaveInstanceState(Bundle icicle) { 531 super.onSaveInstanceState(icicle); 532 // Save list state in the bundle so we can restore it after the 533 // QueryHandler has run 534 icicle.putParcelable(LIST_STATE_KEY, getListView().onSaveInstanceState()); 535 icicle.putBoolean(FOCUS_KEY, getListView().hasFocus()); 536 icicle.putInt(SORT_MODE_KEY, mSortMode); 537 } 538 539 @Override public void onPause() { 540 super.onPause(); 541 stopMediaPlayer(); 542 } 543 544 @Override public void onStop() { 545 super.onStop(); 546 547 // We don't want the list to display the empty state, since when we 548 // resume it will still be there and show up while the new query is 549 // happening. After the async query finishes in response to onResume() 550 // setLoading(false) will be called. 551 mAdapter.setLoading(true); 552 mAdapter.changeCursor(null); 553 } 554 555 /** 556 * Changes the current sort order, building the appropriate query string 557 * for the selected order. 558 */ 559 boolean setSortMode(int sortMode) { 560 if (sortMode != mSortMode) { 561 switch (sortMode) { 562 case TRACK_MENU: 563 mSortMode = sortMode; 564 mSortOrder = MediaStore.Audio.Media.TITLE_KEY; 565 doQuery(false, null); 566 return true; 567 case ALBUM_MENU: 568 mSortMode = sortMode; 569 mSortOrder = MediaStore.Audio.Media.ALBUM_KEY + " ASC, " 570 + MediaStore.Audio.Media.TRACK + " ASC, " 571 + MediaStore.Audio.Media.TITLE_KEY + " ASC"; 572 doQuery(false, null); 573 return true; 574 case ARTIST_MENU: 575 mSortMode = sortMode; 576 mSortOrder = MediaStore.Audio.Media.ARTIST_KEY + " ASC, " 577 + MediaStore.Audio.Media.ALBUM_KEY + " ASC, " 578 + MediaStore.Audio.Media.TRACK + " ASC, " 579 + MediaStore.Audio.Media.TITLE_KEY + " ASC"; 580 doQuery(false, null); 581 return true; 582 } 583 584 } 585 return false; 586 } 587 588 /** 589 * The first time this is called, we hide the large progress indicator 590 * and show the list view, doing fade animations between them. 591 */ 592 void makeListShown() { 593 if (!mListShown) { 594 mListShown = true; 595 mProgressContainer.startAnimation(AnimationUtils.loadAnimation( 596 this, android.R.anim.fade_out)); 597 mProgressContainer.setVisibility(View.GONE); 598 mListContainer.startAnimation(AnimationUtils.loadAnimation( 599 this, android.R.anim.fade_in)); 600 mListContainer.setVisibility(View.VISIBLE); 601 } 602 } 603 604 /** 605 * Common method for performing a query of the music database, called for 606 * both top-level queries and filtering. 607 * 608 * @param sync If true, this query should be done synchronously and the 609 * resulting cursor returned. If false, it will be done asynchronously and 610 * null returned. 611 * @param filterstring If non-null, this is a filter to apply to the query. 612 */ 613 Cursor doQuery(boolean sync, String filterstring) { 614 // Cancel any pending queries 615 mQueryHandler.cancelOperation(MY_QUERY_TOKEN); 616 617 StringBuilder where = new StringBuilder(); 618 where.append(MediaStore.Audio.Media.TITLE + " != ''"); 619 620 // Add in the filtering constraints 621 String [] keywords = null; 622 if (filterstring != null) { 623 String [] searchWords = filterstring.split(" "); 624 keywords = new String[searchWords.length]; 625 Collator col = Collator.getInstance(); 626 col.setStrength(Collator.PRIMARY); 627 for (int i = 0; i < searchWords.length; i++) { 628 keywords[i] = '%' + MediaStore.Audio.keyFor(searchWords[i]) + '%'; 629 } 630 for (int i = 0; i < searchWords.length; i++) { 631 where.append(" AND "); 632 where.append(MediaStore.Audio.Media.ARTIST_KEY + "||"); 633 where.append(MediaStore.Audio.Media.ALBUM_KEY + "||"); 634 where.append(MediaStore.Audio.Media.TITLE_KEY + " LIKE ?"); 635 } 636 } 637 638 // We want to show all audio files, even recordings. Enforcing the 639 // following condition would hide recordings. 640 //where.append(" AND " + MediaStore.Audio.Media.IS_MUSIC + "=1"); 641 642 if (sync) { 643 try { 644 return getContentResolver().query(mBaseUri, CURSOR_COLS, 645 where.toString(), keywords, mSortOrder); 646 } catch (UnsupportedOperationException ex) { 647 } 648 } else { 649 mAdapter.setLoading(true); 650 setProgressBarIndeterminateVisibility(true); 651 mQueryHandler.startQuery(MY_QUERY_TOKEN, null, mBaseUri, CURSOR_COLS, 652 where.toString(), keywords, mSortOrder); 653 } 654 return null; 655 } 656 657 @Override protected void onListItemClick(ListView l, View v, int position, 658 long id) { 659 mCursor.moveToPosition(position); 660 if (DBG) Log.v(TAG, "Click on " + position + " (id=" + id 661 + ", cursid=" 662 + mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID)) 663 + ") in cursor " + mCursor 664 + " adapter=" + l.getAdapter()); 665 setSelected(mCursor); 666 } 667 668 void setSelected(Cursor c) { 669 Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 670 long newId = mCursor.getLong(mCursor.getColumnIndex(MediaStore.Audio.Media._ID)); 671 mSelectedUri = ContentUris.withAppendedId(uri, newId); 672 673 mSelectedId = newId; 674 if (newId != mPlayingId || mMediaPlayer == null) { 675 stopMediaPlayer(); 676 mMediaPlayer = new MediaPlayer(); 677 try { 678 mMediaPlayer.setDataSource(this, mSelectedUri); 679 mMediaPlayer.setOnCompletionListener(this); 680 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_RING); 681 mMediaPlayer.prepare(); 682 mMediaPlayer.start(); 683 mPlayingId = newId; 684 getListView().invalidateViews(); 685 } catch (IOException e) { 686 Log.w("MusicPicker", "Unable to play track", e); 687 } 688 } else if (mMediaPlayer != null) { 689 stopMediaPlayer(); 690 getListView().invalidateViews(); 691 } 692 } 693 694 public void onCompletion(MediaPlayer mp) { 695 if (mMediaPlayer == mp) { 696 mp.stop(); 697 mp.release(); 698 mMediaPlayer = null; 699 mPlayingId = -1; 700 getListView().invalidateViews(); 701 } 702 } 703 704 void stopMediaPlayer() { 705 if (mMediaPlayer != null) { 706 mMediaPlayer.stop(); 707 mMediaPlayer.release(); 708 mMediaPlayer = null; 709 mPlayingId = -1; 710 } 711 } 712 713 public void onClick(View v) { 714 switch (v.getId()) { 715 case R.id.okayButton: 716 if (mSelectedId >= 0) { 717 setResult(RESULT_OK, new Intent().setData(mSelectedUri)); 718 finish(); 719 } 720 break; 721 722 case R.id.cancelButton: 723 finish(); 724 break; 725 } 726 } 727} 728