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