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