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