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