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