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