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