MediaPlaybackActivity.java revision f7b53aba686d8d2819e3fd341097078d85a723be
1/* 2 * Copyright (C) 2007 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.Activity; 20import android.app.AlertDialog; 21import android.app.SearchManager; 22import android.content.BroadcastReceiver; 23import android.content.ComponentName; 24import android.content.ContentResolver; 25import android.content.ContentUris; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.IntentFilter; 30import android.content.ServiceConnection; 31import android.content.res.Configuration; 32import android.content.res.Resources; 33import android.database.Cursor; 34import android.graphics.Bitmap; 35import android.media.AudioManager; 36import android.media.MediaFile; 37import android.net.Uri; 38import android.os.Bundle; 39import android.os.RemoteException; 40import android.os.Handler; 41import android.os.IBinder; 42import android.os.Looper; 43import android.os.Message; 44import android.os.SystemClock; 45import android.provider.MediaStore; 46import android.text.Layout; 47import android.text.TextUtils.TruncateAt; 48import android.util.Log; 49import android.view.KeyEvent; 50import android.view.Menu; 51import android.view.MenuItem; 52import android.view.MotionEvent; 53import android.view.SubMenu; 54import android.view.View; 55import android.view.ViewConfiguration; 56import android.view.Window; 57import android.widget.ImageButton; 58import android.widget.ImageView; 59import android.widget.ProgressBar; 60import android.widget.SeekBar; 61import android.widget.TextView; 62import android.widget.Toast; 63import android.widget.SeekBar.OnSeekBarChangeListener; 64 65 66public class MediaPlaybackActivity extends Activity implements MusicUtils.Defs, 67 View.OnTouchListener, View.OnLongClickListener 68{ 69 private static final int USE_AS_RINGTONE = CHILD_MENU_BASE; 70 71 private boolean mOneShot = false; 72 private boolean mSeeking = false; 73 private boolean mDeviceHasDpad; 74 private long mStartSeekPos = 0; 75 private long mLastSeekEventTime; 76 private IMediaPlaybackService mService = null; 77 private RepeatingImageButton mPrevButton; 78 private ImageButton mPauseButton; 79 private RepeatingImageButton mNextButton; 80 private ImageButton mRepeatButton; 81 private ImageButton mShuffleButton; 82 private ImageButton mQueueButton; 83 private Worker mAlbumArtWorker; 84 private AlbumArtHandler mAlbumArtHandler; 85 private Toast mToast; 86 private int mTouchSlop; 87 88 public MediaPlaybackActivity() 89 { 90 } 91 92 /** Called when the activity is first created. */ 93 @Override 94 public void onCreate(Bundle icicle) 95 { 96 super.onCreate(icicle); 97 setVolumeControlStream(AudioManager.STREAM_MUSIC); 98 99 mAlbumArtWorker = new Worker("album art worker"); 100 mAlbumArtHandler = new AlbumArtHandler(mAlbumArtWorker.getLooper()); 101 102 requestWindowFeature(Window.FEATURE_NO_TITLE); 103 setContentView(R.layout.audio_player); 104 105 mCurrentTime = (TextView) findViewById(R.id.currenttime); 106 mTotalTime = (TextView) findViewById(R.id.totaltime); 107 mProgress = (ProgressBar) findViewById(android.R.id.progress); 108 mAlbum = (ImageView) findViewById(R.id.album); 109 mArtistName = (TextView) findViewById(R.id.artistname); 110 mAlbumName = (TextView) findViewById(R.id.albumname); 111 mTrackName = (TextView) findViewById(R.id.trackname); 112 113 View v = (View)mArtistName.getParent(); 114 v.setOnTouchListener(this); 115 v.setOnLongClickListener(this); 116 117 v = (View)mAlbumName.getParent(); 118 v.setOnTouchListener(this); 119 v.setOnLongClickListener(this); 120 121 v = (View)mTrackName.getParent(); 122 v.setOnTouchListener(this); 123 v.setOnLongClickListener(this); 124 125 mPrevButton = (RepeatingImageButton) findViewById(R.id.prev); 126 mPrevButton.setOnClickListener(mPrevListener); 127 mPrevButton.setRepeatListener(mRewListener, 260); 128 mPauseButton = (ImageButton) findViewById(R.id.pause); 129 mPauseButton.requestFocus(); 130 mPauseButton.setOnClickListener(mPauseListener); 131 mNextButton = (RepeatingImageButton) findViewById(R.id.next); 132 mNextButton.setOnClickListener(mNextListener); 133 mNextButton.setRepeatListener(mFfwdListener, 260); 134 seekmethod = 1; 135 136 mDeviceHasDpad = (getResources().getConfiguration().navigation == 137 Configuration.NAVIGATION_DPAD); 138 139 mQueueButton = (ImageButton) findViewById(R.id.curplaylist); 140 mQueueButton.setOnClickListener(mQueueListener); 141 mShuffleButton = ((ImageButton) findViewById(R.id.shuffle)); 142 mShuffleButton.setOnClickListener(mShuffleListener); 143 mRepeatButton = ((ImageButton) findViewById(R.id.repeat)); 144 mRepeatButton.setOnClickListener(mRepeatListener); 145 146 if (mProgress instanceof SeekBar) { 147 SeekBar seeker = (SeekBar) mProgress; 148 seeker.setOnSeekBarChangeListener(mSeekListener); 149 } 150 mProgress.setMax(1000); 151 152 if (icicle != null) { 153 mOneShot = icicle.getBoolean("oneshot"); 154 } else { 155 mOneShot = getIntent().getBooleanExtra("oneshot", false); 156 } 157 158 mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop(); 159 } 160 161 int mInitialX = -1; 162 int mLastX = -1; 163 int mTextWidth = 0; 164 int mViewWidth = 0; 165 boolean mDraggingLabel = false; 166 167 TextView textViewForContainer(View v) { 168 View vv = v.findViewById(R.id.artistname); 169 if (vv != null) return (TextView) vv; 170 vv = v.findViewById(R.id.albumname); 171 if (vv != null) return (TextView) vv; 172 vv = v.findViewById(R.id.trackname); 173 if (vv != null) return (TextView) vv; 174 return null; 175 } 176 177 public boolean onTouch(View v, MotionEvent event) { 178 int action = event.getAction(); 179 TextView tv = textViewForContainer(v); 180 if (tv == null) { 181 return false; 182 } 183 if (action == MotionEvent.ACTION_DOWN) { 184 v.setBackgroundColor(0xff606060); 185 mInitialX = mLastX = (int) event.getX(); 186 mDraggingLabel = false; 187 } else if (action == MotionEvent.ACTION_UP || 188 action == MotionEvent.ACTION_CANCEL) { 189 v.setBackgroundColor(0); 190 if (mDraggingLabel) { 191 Message msg = mLabelScroller.obtainMessage(0, tv); 192 mLabelScroller.sendMessageDelayed(msg, 1000); 193 } 194 } else if (action == MotionEvent.ACTION_MOVE) { 195 if (mDraggingLabel) { 196 int scrollx = tv.getScrollX(); 197 int x = (int) event.getX(); 198 int delta = mLastX - x; 199 if (delta != 0) { 200 mLastX = x; 201 scrollx += delta; 202 if (scrollx > mTextWidth) { 203 // scrolled the text completely off the view to the left 204 scrollx -= mTextWidth; 205 scrollx -= mViewWidth; 206 } 207 if (scrollx < -mViewWidth) { 208 // scrolled the text completely off the view to the right 209 scrollx += mViewWidth; 210 scrollx += mTextWidth; 211 } 212 tv.scrollTo(scrollx, 0); 213 } 214 return true; 215 } 216 int delta = mInitialX - (int) event.getX(); 217 if (Math.abs(delta) > mTouchSlop) { 218 // start moving 219 mLabelScroller.removeMessages(0, tv); 220 221 // Only turn ellipsizing off when it's not already off, because it 222 // causes the scroll position to be reset to 0. 223 if (tv.getEllipsize() != null) { 224 tv.setEllipsize(null); 225 } 226 Layout ll = tv.getLayout(); 227 // layout might be null if the text just changed, or ellipsizing 228 // was just turned off 229 if (ll == null) { 230 return false; 231 } 232 // get the non-ellipsized line width, to determine whether scrolling 233 // should even be allowed 234 mTextWidth = (int) tv.getLayout().getLineWidth(0); 235 mViewWidth = tv.getWidth(); 236 if (mViewWidth > mTextWidth) { 237 tv.setEllipsize(TruncateAt.END); 238 v.cancelLongPress(); 239 return false; 240 } 241 mDraggingLabel = true; 242 tv.setHorizontalFadingEdgeEnabled(true); 243 v.cancelLongPress(); 244 return true; 245 } 246 } 247 return false; 248 } 249 250 Handler mLabelScroller = new Handler() { 251 @Override 252 public void handleMessage(Message msg) { 253 TextView tv = (TextView) msg.obj; 254 int x = tv.getScrollX(); 255 x = x * 3 / 4; 256 tv.scrollTo(x, 0); 257 if (x == 0) { 258 tv.setEllipsize(TruncateAt.END); 259 } else { 260 Message newmsg = obtainMessage(0, tv); 261 mLabelScroller.sendMessageDelayed(newmsg, 15); 262 } 263 } 264 }; 265 266 public boolean onLongClick(View view) { 267 268 CharSequence title = null; 269 String mime = null; 270 String query = null; 271 String artist; 272 String album; 273 String song; 274 long audioid; 275 276 try { 277 artist = mService.getArtistName(); 278 album = mService.getAlbumName(); 279 song = mService.getTrackName(); 280 audioid = mService.getAudioId(); 281 } catch (RemoteException ex) { 282 return true; 283 } catch (NullPointerException ex) { 284 // we might not actually have the service yet 285 return true; 286 } 287 288 if (MediaFile.UNKNOWN_STRING.equals(album) && 289 MediaFile.UNKNOWN_STRING.equals(artist) && 290 song != null && 291 song.startsWith("recording")) { 292 // not music 293 return false; 294 } 295 296 if (audioid < 0) { 297 return false; 298 } 299 300 Cursor c = MusicUtils.query(this, 301 ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, audioid), 302 new String[] {MediaStore.Audio.Media.IS_MUSIC}, null, null, null); 303 boolean ismusic = true; 304 if (c != null) { 305 if (c.moveToFirst()) { 306 ismusic = c.getInt(0) != 0; 307 } 308 c.close(); 309 } 310 if (!ismusic) { 311 return false; 312 } 313 314 boolean knownartist = 315 (artist != null) && !MediaFile.UNKNOWN_STRING.equals(artist); 316 317 boolean knownalbum = 318 (album != null) && !MediaFile.UNKNOWN_STRING.equals(album); 319 320 if (knownartist && view.equals(mArtistName.getParent())) { 321 title = artist; 322 query = artist; 323 mime = MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE; 324 } else if (knownalbum && view.equals(mAlbumName.getParent())) { 325 title = album; 326 if (knownartist) { 327 query = artist + " " + album; 328 } else { 329 query = album; 330 } 331 mime = MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE; 332 } else if (view.equals(mTrackName.getParent()) || !knownartist || !knownalbum) { 333 if ((song == null) || MediaFile.UNKNOWN_STRING.equals(song)) { 334 // A popup of the form "Search for null/'' using ..." is pretty 335 // unhelpful, plus, we won't find any way to buy it anyway. 336 return true; 337 } 338 339 title = song; 340 if (knownartist) { 341 query = artist + " " + song; 342 } else { 343 query = song; 344 } 345 mime = "audio/*"; // the specific type doesn't matter, so don't bother retrieving it 346 } else { 347 throw new RuntimeException("shouldn't be here"); 348 } 349 title = getString(R.string.mediasearch, title); 350 351 Intent i = new Intent(); 352 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 353 i.setAction(MediaStore.INTENT_ACTION_MEDIA_SEARCH); 354 i.putExtra(SearchManager.QUERY, query); 355 if(knownartist) { 356 i.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist); 357 } 358 if(knownalbum) { 359 i.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album); 360 } 361 i.putExtra(MediaStore.EXTRA_MEDIA_TITLE, song); 362 i.putExtra(MediaStore.EXTRA_MEDIA_FOCUS, mime); 363 364 startActivity(Intent.createChooser(i, title)); 365 return true; 366 } 367 368 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 369 public void onStartTrackingTouch(SeekBar bar) { 370 mLastSeekEventTime = 0; 371 mFromTouch = true; 372 } 373 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 374 if (!fromuser || (mService == null)) return; 375 long now = SystemClock.elapsedRealtime(); 376 if ((now - mLastSeekEventTime) > 250) { 377 mLastSeekEventTime = now; 378 mPosOverride = mDuration * progress / 1000; 379 try { 380 mService.seek(mPosOverride); 381 } catch (RemoteException ex) { 382 } 383 384 // trackball event, allow progress updates 385 if (!mFromTouch) { 386 refreshNow(); 387 mPosOverride = -1; 388 } 389 } 390 } 391 public void onStopTrackingTouch(SeekBar bar) { 392 mPosOverride = -1; 393 mFromTouch = false; 394 } 395 }; 396 397 private View.OnClickListener mQueueListener = new View.OnClickListener() { 398 public void onClick(View v) { 399 startActivity( 400 new Intent(Intent.ACTION_EDIT) 401 .setDataAndType(Uri.EMPTY, "vnd.android.cursor.dir/track") 402 .putExtra("playlist", "nowplaying") 403 ); 404 } 405 }; 406 407 private View.OnClickListener mShuffleListener = new View.OnClickListener() { 408 public void onClick(View v) { 409 toggleShuffle(); 410 } 411 }; 412 413 private View.OnClickListener mRepeatListener = new View.OnClickListener() { 414 public void onClick(View v) { 415 cycleRepeat(); 416 } 417 }; 418 419 private View.OnClickListener mPauseListener = new View.OnClickListener() { 420 public void onClick(View v) { 421 doPauseResume(); 422 } 423 }; 424 425 private View.OnClickListener mPrevListener = new View.OnClickListener() { 426 public void onClick(View v) { 427 if (mService == null) return; 428 try { 429 if (mService.position() < 2000) { 430 mService.prev(); 431 } else { 432 mService.seek(0); 433 mService.play(); 434 } 435 } catch (RemoteException ex) { 436 } 437 } 438 }; 439 440 private View.OnClickListener mNextListener = new View.OnClickListener() { 441 public void onClick(View v) { 442 if (mService == null) return; 443 try { 444 mService.next(); 445 } catch (RemoteException ex) { 446 } 447 } 448 }; 449 450 private RepeatingImageButton.RepeatListener mRewListener = 451 new RepeatingImageButton.RepeatListener() { 452 public void onRepeat(View v, long howlong, int repcnt) { 453 scanBackward(repcnt, howlong); 454 } 455 }; 456 457 private RepeatingImageButton.RepeatListener mFfwdListener = 458 new RepeatingImageButton.RepeatListener() { 459 public void onRepeat(View v, long howlong, int repcnt) { 460 scanForward(repcnt, howlong); 461 } 462 }; 463 464 @Override 465 public void onStop() { 466 paused = true; 467 if (mService != null && mOneShot && getChangingConfigurations() == 0) { 468 try { 469 mService.stop(); 470 } catch (RemoteException ex) { 471 } 472 } 473 mHandler.removeMessages(REFRESH); 474 unregisterReceiver(mStatusListener); 475 MusicUtils.unbindFromService(this); 476 mService = null; 477 super.onStop(); 478 } 479 480 @Override 481 public void onSaveInstanceState(Bundle outState) { 482 outState.putBoolean("oneshot", mOneShot); 483 super.onSaveInstanceState(outState); 484 } 485 486 @Override 487 public void onStart() { 488 super.onStart(); 489 paused = false; 490 491 if (false == MusicUtils.bindToService(this, osc)) { 492 // something went wrong 493 mHandler.sendEmptyMessage(QUIT); 494 } 495 496 IntentFilter f = new IntentFilter(); 497 f.addAction(MediaPlaybackService.PLAYSTATE_CHANGED); 498 f.addAction(MediaPlaybackService.META_CHANGED); 499 f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE); 500 registerReceiver(mStatusListener, new IntentFilter(f)); 501 updateTrackInfo(); 502 long next = refreshNow(); 503 queueNextRefresh(next); 504 } 505 506 @Override 507 public void onNewIntent(Intent intent) { 508 setIntent(intent); 509 mOneShot = intent.getBooleanExtra("oneshot", false); 510 } 511 512 @Override 513 public void onResume() { 514 super.onResume(); 515 updateTrackInfo(); 516 setPauseButtonImage(); 517 } 518 519 @Override 520 public void onDestroy() 521 { 522 mAlbumArtWorker.quit(); 523 super.onDestroy(); 524 //System.out.println("***************** playback activity onDestroy\n"); 525 } 526 527 @Override 528 public boolean onCreateOptionsMenu(Menu menu) { 529 super.onCreateOptionsMenu(menu); 530 // Don't show the menu items if we got launched by path/filedescriptor, or 531 // if we're in one shot mode. In most cases, these menu items are not 532 // useful in those modes, so for consistency we never show them in these 533 // modes, instead of tailoring them to the specific file being played. 534 if (MusicUtils.getCurrentAudioId() >= 0 && !mOneShot) { 535 menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library); 536 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 537 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, 538 R.string.add_to_playlist).setIcon(android.R.drawable.ic_menu_add); 539 menu.add(0, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short).setIcon(R.drawable.ic_menu_set_as_ringtone); 540 menu.add(0, DELETE_ITEM, 0, R.string.delete_item).setIcon(R.drawable.ic_menu_delete); 541 return true; 542 } 543 return false; 544 } 545 546 @Override 547 public boolean onPrepareOptionsMenu(Menu menu) { 548 MenuItem item = menu.findItem(PARTY_SHUFFLE); 549 if (item != null) { 550 int shuffle = MusicUtils.getCurrentShuffleMode(); 551 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) { 552 item.setIcon(R.drawable.ic_menu_party_shuffle); 553 item.setTitle(R.string.party_shuffle_off); 554 } else { 555 item.setIcon(R.drawable.ic_menu_party_shuffle); 556 item.setTitle(R.string.party_shuffle); 557 } 558 } 559 item = menu.findItem(ADD_TO_PLAYLIST); 560 if (item != null) { 561 SubMenu sub = item.getSubMenu(); 562 MusicUtils.makePlaylistMenu(this, sub); 563 } 564 return true; 565 } 566 567 @Override 568 public boolean onOptionsItemSelected(MenuItem item) { 569 Intent intent; 570 try { 571 switch (item.getItemId()) { 572 case GOTO_START: 573 intent = new Intent(); 574 intent.setClass(this, MusicBrowserActivity.class); 575 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 576 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 577 startActivity(intent); 578 break; 579 case USE_AS_RINGTONE: { 580 // Set the system setting to make this the current ringtone 581 if (mService != null) { 582 MusicUtils.setRingtone(this, mService.getAudioId()); 583 } 584 return true; 585 } 586 case PARTY_SHUFFLE: 587 if (mService != null) { 588 int shuffle = mService.getShuffleMode(); 589 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) { 590 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE); 591 } else { 592 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_AUTO); 593 } 594 } 595 setShuffleButtonImage(); 596 break; 597 598 case NEW_PLAYLIST: { 599 intent = new Intent(); 600 intent.setClass(this, CreatePlaylist.class); 601 startActivityForResult(intent, NEW_PLAYLIST); 602 return true; 603 } 604 605 case PLAYLIST_SELECTED: { 606 long [] list = new long[1]; 607 list[0] = MusicUtils.getCurrentAudioId(); 608 long playlist = item.getIntent().getLongExtra("playlist", 0); 609 MusicUtils.addToPlaylist(this, list, playlist); 610 return true; 611 } 612 613 case DELETE_ITEM: { 614 if (mService != null) { 615 long [] list = new long[1]; 616 list[0] = MusicUtils.getCurrentAudioId(); 617 Bundle b = new Bundle(); 618 b.putString("description", getString(R.string.delete_song_desc, 619 mService.getTrackName())); 620 b.putLongArray("items", list); 621 intent = new Intent(); 622 intent.setClass(this, DeleteItems.class); 623 intent.putExtras(b); 624 startActivityForResult(intent, -1); 625 } 626 return true; 627 } 628 } 629 } catch (RemoteException ex) { 630 } 631 return super.onOptionsItemSelected(item); 632 } 633 634 @Override 635 protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 636 if (resultCode != RESULT_OK) { 637 return; 638 } 639 switch (requestCode) { 640 case NEW_PLAYLIST: 641 Uri uri = intent.getData(); 642 if (uri != null) { 643 long [] list = new long[1]; 644 list[0] = MusicUtils.getCurrentAudioId(); 645 int playlist = Integer.parseInt(uri.getLastPathSegment()); 646 MusicUtils.addToPlaylist(this, list, playlist); 647 } 648 break; 649 } 650 } 651 private final int keyboard[][] = { 652 { 653 KeyEvent.KEYCODE_Q, 654 KeyEvent.KEYCODE_W, 655 KeyEvent.KEYCODE_E, 656 KeyEvent.KEYCODE_R, 657 KeyEvent.KEYCODE_T, 658 KeyEvent.KEYCODE_Y, 659 KeyEvent.KEYCODE_U, 660 KeyEvent.KEYCODE_I, 661 KeyEvent.KEYCODE_O, 662 KeyEvent.KEYCODE_P, 663 }, 664 { 665 KeyEvent.KEYCODE_A, 666 KeyEvent.KEYCODE_S, 667 KeyEvent.KEYCODE_D, 668 KeyEvent.KEYCODE_F, 669 KeyEvent.KEYCODE_G, 670 KeyEvent.KEYCODE_H, 671 KeyEvent.KEYCODE_J, 672 KeyEvent.KEYCODE_K, 673 KeyEvent.KEYCODE_L, 674 KeyEvent.KEYCODE_DEL, 675 }, 676 { 677 KeyEvent.KEYCODE_Z, 678 KeyEvent.KEYCODE_X, 679 KeyEvent.KEYCODE_C, 680 KeyEvent.KEYCODE_V, 681 KeyEvent.KEYCODE_B, 682 KeyEvent.KEYCODE_N, 683 KeyEvent.KEYCODE_M, 684 KeyEvent.KEYCODE_COMMA, 685 KeyEvent.KEYCODE_PERIOD, 686 KeyEvent.KEYCODE_ENTER 687 } 688 689 }; 690 691 private int lastX; 692 private int lastY; 693 694 private boolean seekMethod1(int keyCode) 695 { 696 if (mService == null) return false; 697 for(int x=0;x<10;x++) { 698 for(int y=0;y<3;y++) { 699 if(keyboard[y][x] == keyCode) { 700 int dir = 0; 701 // top row 702 if(x == lastX && y == lastY) dir = 0; 703 else if (y == 0 && lastY == 0 && x > lastX) dir = 1; 704 else if (y == 0 && lastY == 0 && x < lastX) dir = -1; 705 // bottom row 706 else if (y == 2 && lastY == 2 && x > lastX) dir = -1; 707 else if (y == 2 && lastY == 2 && x < lastX) dir = 1; 708 // moving up 709 else if (y < lastY && x <= 4) dir = 1; 710 else if (y < lastY && x >= 5) dir = -1; 711 // moving down 712 else if (y > lastY && x <= 4) dir = -1; 713 else if (y > lastY && x >= 5) dir = 1; 714 lastX = x; 715 lastY = y; 716 try { 717 mService.seek(mService.position() + dir * 5); 718 } catch (RemoteException ex) { 719 } 720 refreshNow(); 721 return true; 722 } 723 } 724 } 725 lastX = -1; 726 lastY = -1; 727 return false; 728 } 729 730 private boolean seekMethod2(int keyCode) 731 { 732 if (mService == null) return false; 733 for(int i=0;i<10;i++) { 734 if(keyboard[0][i] == keyCode) { 735 int seekpercentage = 100*i/10; 736 try { 737 mService.seek(mService.duration() * seekpercentage / 100); 738 } catch (RemoteException ex) { 739 } 740 refreshNow(); 741 return true; 742 } 743 } 744 return false; 745 } 746 747 @Override 748 public boolean onKeyUp(int keyCode, KeyEvent event) { 749 try { 750 switch(keyCode) 751 { 752 case KeyEvent.KEYCODE_DPAD_LEFT: 753 if (!useDpadMusicControl()) { 754 break; 755 } 756 if (mService != null) { 757 if (!mSeeking && mStartSeekPos >= 0) { 758 mPauseButton.requestFocus(); 759 if (mStartSeekPos < 1000) { 760 mService.prev(); 761 } else { 762 mService.seek(0); 763 } 764 } else { 765 scanBackward(-1, event.getEventTime() - event.getDownTime()); 766 mPauseButton.requestFocus(); 767 mStartSeekPos = -1; 768 } 769 } 770 mSeeking = false; 771 mPosOverride = -1; 772 return true; 773 case KeyEvent.KEYCODE_DPAD_RIGHT: 774 if (!useDpadMusicControl()) { 775 break; 776 } 777 if (mService != null) { 778 if (!mSeeking && mStartSeekPos >= 0) { 779 mPauseButton.requestFocus(); 780 mService.next(); 781 } else { 782 scanForward(-1, event.getEventTime() - event.getDownTime()); 783 mPauseButton.requestFocus(); 784 mStartSeekPos = -1; 785 } 786 } 787 mSeeking = false; 788 mPosOverride = -1; 789 return true; 790 } 791 } catch (RemoteException ex) { 792 } 793 return super.onKeyUp(keyCode, event); 794 } 795 796 private boolean useDpadMusicControl() { 797 if (mDeviceHasDpad && (mPrevButton.isFocused() || 798 mNextButton.isFocused() || 799 mPauseButton.isFocused())) { 800 return true; 801 } 802 return false; 803 } 804 805 @Override 806 public boolean onKeyDown(int keyCode, KeyEvent event) 807 { 808 int direction = -1; 809 int repcnt = event.getRepeatCount(); 810 811 if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode)) 812 return true; 813 814 switch(keyCode) 815 { 816/* 817 // image scale 818 case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break; 819 case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break; 820 // image translate 821 case KeyEvent.KEYCODE_W: av.adjustParams( 0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break; 822 case KeyEvent.KEYCODE_X: av.adjustParams( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break; 823 case KeyEvent.KEYCODE_A: av.adjustParams( 0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break; 824 case KeyEvent.KEYCODE_D: av.adjustParams( 0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break; 825 // camera rotation 826 case KeyEvent.KEYCODE_R: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break; 827 case KeyEvent.KEYCODE_U: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break; 828 // camera translate 829 case KeyEvent.KEYCODE_Y: av.adjustParams( 0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break; 830 case KeyEvent.KEYCODE_N: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break; 831 case KeyEvent.KEYCODE_G: av.adjustParams( 0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break; 832 case KeyEvent.KEYCODE_J: av.adjustParams( 0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break; 833 834*/ 835 836 case KeyEvent.KEYCODE_SLASH: 837 seekmethod = 1 - seekmethod; 838 return true; 839 840 case KeyEvent.KEYCODE_DPAD_LEFT: 841 if (!useDpadMusicControl()) { 842 break; 843 } 844 if (!mPrevButton.hasFocus()) { 845 mPrevButton.requestFocus(); 846 } 847 scanBackward(repcnt, event.getEventTime() - event.getDownTime()); 848 return true; 849 case KeyEvent.KEYCODE_DPAD_RIGHT: 850 if (!useDpadMusicControl()) { 851 break; 852 } 853 if (!mNextButton.hasFocus()) { 854 mNextButton.requestFocus(); 855 } 856 scanForward(repcnt, event.getEventTime() - event.getDownTime()); 857 return true; 858 859 case KeyEvent.KEYCODE_S: 860 toggleShuffle(); 861 return true; 862 863 case KeyEvent.KEYCODE_DPAD_CENTER: 864 case KeyEvent.KEYCODE_SPACE: 865 doPauseResume(); 866 return true; 867 } 868 return super.onKeyDown(keyCode, event); 869 } 870 871 private void scanBackward(int repcnt, long delta) { 872 if(mService == null) return; 873 try { 874 if(repcnt == 0) { 875 mStartSeekPos = mService.position(); 876 mLastSeekEventTime = 0; 877 mSeeking = false; 878 } else { 879 mSeeking = true; 880 if (delta < 5000) { 881 // seek at 10x speed for the first 5 seconds 882 delta = delta * 10; 883 } else { 884 // seek at 40x after that 885 delta = 50000 + (delta - 5000) * 40; 886 } 887 long newpos = mStartSeekPos - delta; 888 if (newpos < 0) { 889 // move to previous track 890 mService.prev(); 891 long duration = mService.duration(); 892 mStartSeekPos += duration; 893 newpos += duration; 894 } 895 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){ 896 mService.seek(newpos); 897 mLastSeekEventTime = delta; 898 } 899 if (repcnt >= 0) { 900 mPosOverride = newpos; 901 } else { 902 mPosOverride = -1; 903 } 904 refreshNow(); 905 } 906 } catch (RemoteException ex) { 907 } 908 } 909 910 private void scanForward(int repcnt, long delta) { 911 if(mService == null) return; 912 try { 913 if(repcnt == 0) { 914 mStartSeekPos = mService.position(); 915 mLastSeekEventTime = 0; 916 mSeeking = false; 917 } else { 918 mSeeking = true; 919 if (delta < 5000) { 920 // seek at 10x speed for the first 5 seconds 921 delta = delta * 10; 922 } else { 923 // seek at 40x after that 924 delta = 50000 + (delta - 5000) * 40; 925 } 926 long newpos = mStartSeekPos + delta; 927 long duration = mService.duration(); 928 if (newpos >= duration) { 929 // move to next track 930 mService.next(); 931 mStartSeekPos -= duration; // is OK to go negative 932 newpos -= duration; 933 } 934 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){ 935 mService.seek(newpos); 936 mLastSeekEventTime = delta; 937 } 938 if (repcnt >= 0) { 939 mPosOverride = newpos; 940 } else { 941 mPosOverride = -1; 942 } 943 refreshNow(); 944 } 945 } catch (RemoteException ex) { 946 } 947 } 948 949 private void doPauseResume() { 950 try { 951 if(mService != null) { 952 if (mService.isPlaying()) { 953 mService.pause(); 954 } else { 955 mService.play(); 956 } 957 refreshNow(); 958 setPauseButtonImage(); 959 } 960 } catch (RemoteException ex) { 961 } 962 } 963 964 private void toggleShuffle() { 965 if (mService == null) { 966 return; 967 } 968 try { 969 int shuffle = mService.getShuffleMode(); 970 if (shuffle == MediaPlaybackService.SHUFFLE_NONE) { 971 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL); 972 if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) { 973 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL); 974 setRepeatButtonImage(); 975 } 976 showToast(R.string.shuffle_on_notif); 977 } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL || 978 shuffle == MediaPlaybackService.SHUFFLE_AUTO) { 979 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE); 980 showToast(R.string.shuffle_off_notif); 981 } else { 982 Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle); 983 } 984 setShuffleButtonImage(); 985 } catch (RemoteException ex) { 986 } 987 } 988 989 private void cycleRepeat() { 990 if (mService == null) { 991 return; 992 } 993 try { 994 int mode = mService.getRepeatMode(); 995 if (mode == MediaPlaybackService.REPEAT_NONE) { 996 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL); 997 showToast(R.string.repeat_all_notif); 998 } else if (mode == MediaPlaybackService.REPEAT_ALL) { 999 mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT); 1000 if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) { 1001 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE); 1002 setShuffleButtonImage(); 1003 } 1004 showToast(R.string.repeat_current_notif); 1005 } else { 1006 mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE); 1007 showToast(R.string.repeat_off_notif); 1008 } 1009 setRepeatButtonImage(); 1010 } catch (RemoteException ex) { 1011 } 1012 1013 } 1014 1015 private void showToast(int resid) { 1016 if (mToast == null) { 1017 mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT); 1018 } 1019 mToast.setText(resid); 1020 mToast.show(); 1021 } 1022 1023 private void startPlayback() { 1024 1025 if(mService == null) 1026 return; 1027 Intent intent = getIntent(); 1028 String filename = ""; 1029 Uri uri = intent.getData(); 1030 if (uri != null && uri.toString().length() > 0) { 1031 // If this is a file:// URI, just use the path directly instead 1032 // of going through the open-from-filedescriptor codepath. 1033 String scheme = uri.getScheme(); 1034 if ("file".equals(scheme)) { 1035 filename = uri.getPath(); 1036 } else { 1037 filename = uri.toString(); 1038 } 1039 try { 1040 if (! ContentResolver.SCHEME_CONTENT.equals(scheme) || 1041 ! MediaStore.AUTHORITY.equals(uri.getAuthority())) { 1042 mOneShot = true; 1043 } 1044 mService.stop(); 1045 mService.openFile(filename, mOneShot); 1046 mService.play(); 1047 setIntent(new Intent()); 1048 } catch (Exception ex) { 1049 Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex); 1050 } 1051 } 1052 1053 updateTrackInfo(); 1054 long next = refreshNow(); 1055 queueNextRefresh(next); 1056 } 1057 1058 private ServiceConnection osc = new ServiceConnection() { 1059 public void onServiceConnected(ComponentName classname, IBinder obj) { 1060 mService = IMediaPlaybackService.Stub.asInterface(obj); 1061 startPlayback(); 1062 try { 1063 // Assume something is playing when the service says it is, 1064 // but also if the audio ID is valid but the service is paused. 1065 if (mService.getAudioId() >= 0 || mService.isPlaying() || 1066 mService.getPath() != null) { 1067 // something is playing now, we're done 1068 if (mOneShot || mService.getAudioId() < 0) { 1069 mRepeatButton.setVisibility(View.INVISIBLE); 1070 mShuffleButton.setVisibility(View.INVISIBLE); 1071 mQueueButton.setVisibility(View.INVISIBLE); 1072 } else { 1073 mRepeatButton.setVisibility(View.VISIBLE); 1074 mShuffleButton.setVisibility(View.VISIBLE); 1075 mQueueButton.setVisibility(View.VISIBLE); 1076 setRepeatButtonImage(); 1077 setShuffleButtonImage(); 1078 } 1079 setPauseButtonImage(); 1080 return; 1081 } 1082 } catch (RemoteException ex) { 1083 } 1084 // Service is dead or not playing anything. If we got here as part 1085 // of a "play this file" Intent, exit. Otherwise go to the Music 1086 // app start screen. 1087 if (getIntent().getData() == null) { 1088 Intent intent = new Intent(Intent.ACTION_MAIN); 1089 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1090 intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class); 1091 startActivity(intent); 1092 } 1093 finish(); 1094 } 1095 public void onServiceDisconnected(ComponentName classname) { 1096 } 1097 }; 1098 1099 private void setRepeatButtonImage() { 1100 try { 1101 switch (mService.getRepeatMode()) { 1102 case MediaPlaybackService.REPEAT_ALL: 1103 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn); 1104 break; 1105 case MediaPlaybackService.REPEAT_CURRENT: 1106 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn); 1107 break; 1108 default: 1109 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn); 1110 break; 1111 } 1112 } catch (RemoteException ex) { 1113 } 1114 } 1115 1116 private void setShuffleButtonImage() { 1117 try { 1118 switch (mService.getShuffleMode()) { 1119 case MediaPlaybackService.SHUFFLE_NONE: 1120 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn); 1121 break; 1122 case MediaPlaybackService.SHUFFLE_AUTO: 1123 mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn); 1124 break; 1125 default: 1126 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn); 1127 break; 1128 } 1129 } catch (RemoteException ex) { 1130 } 1131 } 1132 1133 private void setPauseButtonImage() { 1134 try { 1135 if (mService != null && mService.isPlaying()) { 1136 mPauseButton.setImageResource(android.R.drawable.ic_media_pause); 1137 } else { 1138 mPauseButton.setImageResource(android.R.drawable.ic_media_play); 1139 } 1140 } catch (RemoteException ex) { 1141 } 1142 } 1143 1144 private ImageView mAlbum; 1145 private TextView mCurrentTime; 1146 private TextView mTotalTime; 1147 private TextView mArtistName; 1148 private TextView mAlbumName; 1149 private TextView mTrackName; 1150 private ProgressBar mProgress; 1151 private long mPosOverride = -1; 1152 private boolean mFromTouch = false; 1153 private long mDuration; 1154 private int seekmethod; 1155 private boolean paused; 1156 1157 private static final int REFRESH = 1; 1158 private static final int QUIT = 2; 1159 private static final int GET_ALBUM_ART = 3; 1160 private static final int ALBUM_ART_DECODED = 4; 1161 1162 private void queueNextRefresh(long delay) { 1163 if (!paused) { 1164 Message msg = mHandler.obtainMessage(REFRESH); 1165 mHandler.removeMessages(REFRESH); 1166 mHandler.sendMessageDelayed(msg, delay); 1167 } 1168 } 1169 1170 private long refreshNow() { 1171 if(mService == null) 1172 return 500; 1173 try { 1174 long pos = mPosOverride < 0 ? mService.position() : mPosOverride; 1175 long remaining = 1000 - (pos % 1000); 1176 if ((pos >= 0) && (mDuration > 0)) { 1177 mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000)); 1178 1179 if (mService.isPlaying()) { 1180 mCurrentTime.setVisibility(View.VISIBLE); 1181 } else { 1182 // blink the counter 1183 int vis = mCurrentTime.getVisibility(); 1184 mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE); 1185 remaining = 500; 1186 } 1187 1188 mProgress.setProgress((int) (1000 * pos / mDuration)); 1189 } else { 1190 mCurrentTime.setText("--:--"); 1191 mProgress.setProgress(1000); 1192 } 1193 // return the number of milliseconds until the next full second, so 1194 // the counter can be updated at just the right time 1195 return remaining; 1196 } catch (RemoteException ex) { 1197 } 1198 return 500; 1199 } 1200 1201 private final Handler mHandler = new Handler() { 1202 @Override 1203 public void handleMessage(Message msg) { 1204 switch (msg.what) { 1205 case ALBUM_ART_DECODED: 1206 mAlbum.setImageBitmap((Bitmap)msg.obj); 1207 mAlbum.getDrawable().setDither(true); 1208 break; 1209 1210 case REFRESH: 1211 long next = refreshNow(); 1212 queueNextRefresh(next); 1213 break; 1214 1215 case QUIT: 1216 // This can be moved back to onCreate once the bug that prevents 1217 // Dialogs from being started from onCreate/onResume is fixed. 1218 new AlertDialog.Builder(MediaPlaybackActivity.this) 1219 .setTitle(R.string.service_start_error_title) 1220 .setMessage(R.string.service_start_error_msg) 1221 .setPositiveButton(R.string.service_start_error_button, 1222 new DialogInterface.OnClickListener() { 1223 public void onClick(DialogInterface dialog, int whichButton) { 1224 finish(); 1225 } 1226 }) 1227 .setCancelable(false) 1228 .show(); 1229 break; 1230 1231 default: 1232 break; 1233 } 1234 } 1235 }; 1236 1237 private BroadcastReceiver mStatusListener = new BroadcastReceiver() { 1238 @Override 1239 public void onReceive(Context context, Intent intent) { 1240 String action = intent.getAction(); 1241 if (action.equals(MediaPlaybackService.META_CHANGED)) { 1242 // redraw the artist/title info and 1243 // set new max for progress bar 1244 updateTrackInfo(); 1245 setPauseButtonImage(); 1246 queueNextRefresh(1); 1247 } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE)) { 1248 if (mOneShot) { 1249 finish(); 1250 } else { 1251 setPauseButtonImage(); 1252 } 1253 } else if (action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) { 1254 setPauseButtonImage(); 1255 } 1256 } 1257 }; 1258 1259 private static class AlbumSongIdWrapper { 1260 public long albumid; 1261 public long songid; 1262 AlbumSongIdWrapper(long aid, long sid) { 1263 albumid = aid; 1264 songid = sid; 1265 } 1266 } 1267 1268 private void updateTrackInfo() { 1269 if (mService == null) { 1270 return; 1271 } 1272 try { 1273 String path = mService.getPath(); 1274 if (path == null) { 1275 finish(); 1276 return; 1277 } 1278 1279 long songid = mService.getAudioId(); 1280 if (songid < 0 && path.toLowerCase().startsWith("http://")) { 1281 // Once we can get album art and meta data from MediaPlayer, we 1282 // can show that info again when streaming. 1283 ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE); 1284 ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE); 1285 mAlbum.setVisibility(View.GONE); 1286 mTrackName.setText(path); 1287 mAlbumArtHandler.removeMessages(GET_ALBUM_ART); 1288 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(-1, -1)).sendToTarget(); 1289 } else { 1290 ((View) mArtistName.getParent()).setVisibility(View.VISIBLE); 1291 ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE); 1292 String artistName = mService.getArtistName(); 1293 if (MediaFile.UNKNOWN_STRING.equals(artistName)) { 1294 artistName = getString(R.string.unknown_artist_name); 1295 } 1296 mArtistName.setText(artistName); 1297 String albumName = mService.getAlbumName(); 1298 long albumid = mService.getAlbumId(); 1299 if (MediaFile.UNKNOWN_STRING.equals(albumName)) { 1300 albumName = getString(R.string.unknown_album_name); 1301 albumid = -1; 1302 } 1303 mAlbumName.setText(albumName); 1304 mTrackName.setText(mService.getTrackName()); 1305 mAlbumArtHandler.removeMessages(GET_ALBUM_ART); 1306 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(albumid, songid)).sendToTarget(); 1307 mAlbum.setVisibility(View.VISIBLE); 1308 } 1309 mDuration = mService.duration(); 1310 mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000)); 1311 } catch (RemoteException ex) { 1312 finish(); 1313 } 1314 } 1315 1316 public class AlbumArtHandler extends Handler { 1317 private long mAlbumId = -1; 1318 1319 public AlbumArtHandler(Looper looper) { 1320 super(looper); 1321 } 1322 @Override 1323 public void handleMessage(Message msg) 1324 { 1325 long albumid = ((AlbumSongIdWrapper) msg.obj).albumid; 1326 long songid = ((AlbumSongIdWrapper) msg.obj).songid; 1327 if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) { 1328 // while decoding the new image, show the default album art 1329 Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null); 1330 mHandler.removeMessages(ALBUM_ART_DECODED); 1331 mHandler.sendMessageDelayed(numsg, 300); 1332 Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, albumid); 1333 if (bm == null) { 1334 bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, -1); 1335 albumid = -1; 1336 } 1337 if (bm != null) { 1338 numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm); 1339 mHandler.removeMessages(ALBUM_ART_DECODED); 1340 mHandler.sendMessage(numsg); 1341 } 1342 mAlbumId = albumid; 1343 } 1344 } 1345 } 1346 1347 private static class Worker implements Runnable { 1348 private final Object mLock = new Object(); 1349 private Looper mLooper; 1350 1351 /** 1352 * Creates a worker thread with the given name. The thread 1353 * then runs a {@link android.os.Looper}. 1354 * @param name A name for the new thread 1355 */ 1356 Worker(String name) { 1357 Thread t = new Thread(null, this, name); 1358 t.setPriority(Thread.MIN_PRIORITY); 1359 t.start(); 1360 synchronized (mLock) { 1361 while (mLooper == null) { 1362 try { 1363 mLock.wait(); 1364 } catch (InterruptedException ex) { 1365 } 1366 } 1367 } 1368 } 1369 1370 public Looper getLooper() { 1371 return mLooper; 1372 } 1373 1374 public void run() { 1375 synchronized (mLock) { 1376 Looper.prepare(); 1377 mLooper = Looper.myLooper(); 1378 mLock.notifyAll(); 1379 } 1380 Looper.loop(); 1381 } 1382 1383 public void quit() { 1384 mLooper.quit(); 1385 } 1386 } 1387} 1388 1389