MediaPlaybackActivity.java revision 7c065604aeca9d4de9efa7426dfb7827c9291fd8
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 f.addAction(MediaPlaybackService.PLAYBACK_COMPLETE); 486 registerReceiver(mStatusListener, new IntentFilter(f)); 487 updateTrackInfo(); 488 long next = refreshNow(); 489 queueNextRefresh(next); 490 } 491 492 @Override 493 public void onNewIntent(Intent intent) { 494 setIntent(intent); 495 } 496 497 @Override 498 public void onResume() { 499 super.onResume(); 500 updateTrackInfo(); 501 setPauseButtonImage(); 502 } 503 504 @Override 505 public void onDestroy() 506 { 507 mAlbumArtWorker.quit(); 508 super.onDestroy(); 509 //System.out.println("***************** playback activity onDestroy\n"); 510 } 511 512 @Override 513 public boolean onCreateOptionsMenu(Menu menu) { 514 super.onCreateOptionsMenu(menu); 515 // Don't show the menu items if we got launched by path/filedescriptor, or 516 // if we're in one shot mode. In most cases, these menu items are not 517 // useful in those modes, so for consistency we never show them in these 518 // modes, instead of tailoring them to the specific file being played. 519 if (MusicUtils.getCurrentAudioId() >= 0) { 520 menu.add(0, GOTO_START, 0, R.string.goto_start).setIcon(R.drawable.ic_menu_music_library); 521 menu.add(0, PARTY_SHUFFLE, 0, R.string.party_shuffle); // icon will be set in onPrepareOptionsMenu() 522 SubMenu sub = menu.addSubMenu(0, ADD_TO_PLAYLIST, 0, 523 R.string.add_to_playlist).setIcon(android.R.drawable.ic_menu_add); 524 // these next two are in a separate group, so they can be shown/hidden as needed 525 // based on the keyguard state 526 menu.add(1, USE_AS_RINGTONE, 0, R.string.ringtone_menu_short) 527 .setIcon(R.drawable.ic_menu_set_as_ringtone); 528 menu.add(1, DELETE_ITEM, 0, R.string.delete_item) 529 .setIcon(R.drawable.ic_menu_delete); 530 531 Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); 532 if (getPackageManager().resolveActivity(i, 0) != null) { 533 menu.add(0, EFFECTS_PANEL, 0, R.string.effectspanel).setIcon(R.drawable.ic_menu_eq); 534 } 535 536 return true; 537 } 538 return false; 539 } 540 541 @Override 542 public boolean onPrepareOptionsMenu(Menu menu) { 543 if (mService == null) return false; 544 MenuItem item = menu.findItem(PARTY_SHUFFLE); 545 if (item != null) { 546 int shuffle = MusicUtils.getCurrentShuffleMode(); 547 if (shuffle == MediaPlaybackService.SHUFFLE_AUTO) { 548 item.setIcon(R.drawable.ic_menu_party_shuffle); 549 item.setTitle(R.string.party_shuffle_off); 550 } else { 551 item.setIcon(R.drawable.ic_menu_party_shuffle); 552 item.setTitle(R.string.party_shuffle); 553 } 554 } 555 556 item = menu.findItem(ADD_TO_PLAYLIST); 557 if (item != null) { 558 SubMenu sub = item.getSubMenu(); 559 MusicUtils.makePlaylistMenu(this, sub); 560 } 561 562 KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 563 menu.setGroupVisible(1, !km.inKeyguardRestrictedInputMode()); 564 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_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); 577 startActivity(intent); 578 finish(); 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 MusicUtils.togglePartyShuffle(); 589 setShuffleButtonImage(); 590 break; 591 592 case NEW_PLAYLIST: { 593 intent = new Intent(); 594 intent.setClass(this, CreatePlaylist.class); 595 startActivityForResult(intent, NEW_PLAYLIST); 596 return true; 597 } 598 599 case PLAYLIST_SELECTED: { 600 long [] list = new long[1]; 601 list[0] = MusicUtils.getCurrentAudioId(); 602 long playlist = item.getIntent().getLongExtra("playlist", 0); 603 MusicUtils.addToPlaylist(this, list, playlist); 604 return true; 605 } 606 607 case DELETE_ITEM: { 608 if (mService != null) { 609 long [] list = new long[1]; 610 list[0] = MusicUtils.getCurrentAudioId(); 611 Bundle b = new Bundle(); 612 b.putString("description", getString(R.string.delete_song_desc, 613 mService.getTrackName())); 614 b.putLongArray("items", list); 615 intent = new Intent(); 616 intent.setClass(this, DeleteItems.class); 617 intent.putExtras(b); 618 startActivityForResult(intent, -1); 619 } 620 return true; 621 } 622 623 case EFFECTS_PANEL: { 624 Intent i = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); 625 i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, mService.getAudioSessionId()); 626 startActivityForResult(i, EFFECTS_PANEL); 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 if (mService == null) return false; 698 for(int x=0;x<10;x++) { 699 for(int y=0;y<3;y++) { 700 if(keyboard[y][x] == keyCode) { 701 int dir = 0; 702 // top row 703 if(x == lastX && y == lastY) dir = 0; 704 else if (y == 0 && lastY == 0 && x > lastX) dir = 1; 705 else if (y == 0 && lastY == 0 && x < lastX) dir = -1; 706 // bottom row 707 else if (y == 2 && lastY == 2 && x > lastX) dir = -1; 708 else if (y == 2 && lastY == 2 && x < lastX) dir = 1; 709 // moving up 710 else if (y < lastY && x <= 4) dir = 1; 711 else if (y < lastY && x >= 5) dir = -1; 712 // moving down 713 else if (y > lastY && x <= 4) dir = -1; 714 else if (y > lastY && x >= 5) dir = 1; 715 lastX = x; 716 lastY = y; 717 try { 718 mService.seek(mService.position() + dir * 5); 719 } catch (RemoteException ex) { 720 } 721 refreshNow(); 722 return true; 723 } 724 } 725 } 726 lastX = -1; 727 lastY = -1; 728 return false; 729 } 730 731 private boolean seekMethod2(int keyCode) 732 { 733 if (mService == null) return false; 734 for(int i=0;i<10;i++) { 735 if(keyboard[0][i] == keyCode) { 736 int seekpercentage = 100*i/10; 737 try { 738 mService.seek(mService.duration() * seekpercentage / 100); 739 } catch (RemoteException ex) { 740 } 741 refreshNow(); 742 return true; 743 } 744 } 745 return false; 746 } 747 748 @Override 749 public boolean onKeyUp(int keyCode, KeyEvent event) { 750 try { 751 switch(keyCode) 752 { 753 case KeyEvent.KEYCODE_DPAD_LEFT: 754 if (!useDpadMusicControl()) { 755 break; 756 } 757 if (mService != null) { 758 if (!mSeeking && mStartSeekPos >= 0) { 759 mPauseButton.requestFocus(); 760 if (mStartSeekPos < 1000) { 761 mService.prev(); 762 } else { 763 mService.seek(0); 764 } 765 } else { 766 scanBackward(-1, event.getEventTime() - event.getDownTime()); 767 mPauseButton.requestFocus(); 768 mStartSeekPos = -1; 769 } 770 } 771 mSeeking = false; 772 mPosOverride = -1; 773 return true; 774 case KeyEvent.KEYCODE_DPAD_RIGHT: 775 if (!useDpadMusicControl()) { 776 break; 777 } 778 if (mService != null) { 779 if (!mSeeking && mStartSeekPos >= 0) { 780 mPauseButton.requestFocus(); 781 mService.next(); 782 } else { 783 scanForward(-1, event.getEventTime() - event.getDownTime()); 784 mPauseButton.requestFocus(); 785 mStartSeekPos = -1; 786 } 787 } 788 mSeeking = false; 789 mPosOverride = -1; 790 return true; 791 } 792 } catch (RemoteException ex) { 793 } 794 return super.onKeyUp(keyCode, event); 795 } 796 797 private boolean useDpadMusicControl() { 798 if (mDeviceHasDpad && (mPrevButton.isFocused() || 799 mNextButton.isFocused() || 800 mPauseButton.isFocused())) { 801 return true; 802 } 803 return false; 804 } 805 806 @Override 807 public boolean onKeyDown(int keyCode, KeyEvent event) 808 { 809 int direction = -1; 810 int repcnt = event.getRepeatCount(); 811 812 if((seekmethod==0)?seekMethod1(keyCode):seekMethod2(keyCode)) 813 return true; 814 815 switch(keyCode) 816 { 817/* 818 // image scale 819 case KeyEvent.KEYCODE_Q: av.adjustParams(-0.05, 0.0, 0.0, 0.0, 0.0,-1.0); break; 820 case KeyEvent.KEYCODE_E: av.adjustParams( 0.05, 0.0, 0.0, 0.0, 0.0, 1.0); break; 821 // image translate 822 case KeyEvent.KEYCODE_W: av.adjustParams( 0.0, 0.0,-1.0, 0.0, 0.0, 0.0); break; 823 case KeyEvent.KEYCODE_X: av.adjustParams( 0.0, 0.0, 1.0, 0.0, 0.0, 0.0); break; 824 case KeyEvent.KEYCODE_A: av.adjustParams( 0.0,-1.0, 0.0, 0.0, 0.0, 0.0); break; 825 case KeyEvent.KEYCODE_D: av.adjustParams( 0.0, 1.0, 0.0, 0.0, 0.0, 0.0); break; 826 // camera rotation 827 case KeyEvent.KEYCODE_R: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0,-1.0); break; 828 case KeyEvent.KEYCODE_U: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 0.0, 1.0); break; 829 // camera translate 830 case KeyEvent.KEYCODE_Y: av.adjustParams( 0.0, 0.0, 0.0, 0.0,-1.0, 0.0); break; 831 case KeyEvent.KEYCODE_N: av.adjustParams( 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); break; 832 case KeyEvent.KEYCODE_G: av.adjustParams( 0.0, 0.0, 0.0,-1.0, 0.0, 0.0); break; 833 case KeyEvent.KEYCODE_J: av.adjustParams( 0.0, 0.0, 0.0, 1.0, 0.0, 0.0); break; 834 835*/ 836 837 case KeyEvent.KEYCODE_SLASH: 838 seekmethod = 1 - seekmethod; 839 return true; 840 841 case KeyEvent.KEYCODE_DPAD_LEFT: 842 if (!useDpadMusicControl()) { 843 break; 844 } 845 if (!mPrevButton.hasFocus()) { 846 mPrevButton.requestFocus(); 847 } 848 scanBackward(repcnt, event.getEventTime() - event.getDownTime()); 849 return true; 850 case KeyEvent.KEYCODE_DPAD_RIGHT: 851 if (!useDpadMusicControl()) { 852 break; 853 } 854 if (!mNextButton.hasFocus()) { 855 mNextButton.requestFocus(); 856 } 857 scanForward(repcnt, event.getEventTime() - event.getDownTime()); 858 return true; 859 860 case KeyEvent.KEYCODE_S: 861 toggleShuffle(); 862 return true; 863 864 case KeyEvent.KEYCODE_DPAD_CENTER: 865 case KeyEvent.KEYCODE_SPACE: 866 doPauseResume(); 867 return true; 868 } 869 return super.onKeyDown(keyCode, event); 870 } 871 872 private void scanBackward(int repcnt, long delta) { 873 if(mService == null) return; 874 try { 875 if(repcnt == 0) { 876 mStartSeekPos = mService.position(); 877 mLastSeekEventTime = 0; 878 mSeeking = false; 879 } else { 880 mSeeking = true; 881 if (delta < 5000) { 882 // seek at 10x speed for the first 5 seconds 883 delta = delta * 10; 884 } else { 885 // seek at 40x after that 886 delta = 50000 + (delta - 5000) * 40; 887 } 888 long newpos = mStartSeekPos - delta; 889 if (newpos < 0) { 890 // move to previous track 891 mService.prev(); 892 long duration = mService.duration(); 893 mStartSeekPos += duration; 894 newpos += duration; 895 } 896 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){ 897 mService.seek(newpos); 898 mLastSeekEventTime = delta; 899 } 900 if (repcnt >= 0) { 901 mPosOverride = newpos; 902 } else { 903 mPosOverride = -1; 904 } 905 refreshNow(); 906 } 907 } catch (RemoteException ex) { 908 } 909 } 910 911 private void scanForward(int repcnt, long delta) { 912 if(mService == null) return; 913 try { 914 if(repcnt == 0) { 915 mStartSeekPos = mService.position(); 916 mLastSeekEventTime = 0; 917 mSeeking = false; 918 } else { 919 mSeeking = true; 920 if (delta < 5000) { 921 // seek at 10x speed for the first 5 seconds 922 delta = delta * 10; 923 } else { 924 // seek at 40x after that 925 delta = 50000 + (delta - 5000) * 40; 926 } 927 long newpos = mStartSeekPos + delta; 928 long duration = mService.duration(); 929 if (newpos >= duration) { 930 // move to next track 931 mService.next(); 932 mStartSeekPos -= duration; // is OK to go negative 933 newpos -= duration; 934 } 935 if (((delta - mLastSeekEventTime) > 250) || repcnt < 0){ 936 mService.seek(newpos); 937 mLastSeekEventTime = delta; 938 } 939 if (repcnt >= 0) { 940 mPosOverride = newpos; 941 } else { 942 mPosOverride = -1; 943 } 944 refreshNow(); 945 } 946 } catch (RemoteException ex) { 947 } 948 } 949 950 private void doPauseResume() { 951 try { 952 if(mService != null) { 953 if (mService.isPlaying()) { 954 mService.pause(); 955 } else { 956 mService.play(); 957 } 958 refreshNow(); 959 setPauseButtonImage(); 960 } 961 } catch (RemoteException ex) { 962 } 963 } 964 965 private void toggleShuffle() { 966 if (mService == null) { 967 return; 968 } 969 try { 970 int shuffle = mService.getShuffleMode(); 971 if (shuffle == MediaPlaybackService.SHUFFLE_NONE) { 972 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NORMAL); 973 if (mService.getRepeatMode() == MediaPlaybackService.REPEAT_CURRENT) { 974 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL); 975 setRepeatButtonImage(); 976 } 977 showToast(R.string.shuffle_on_notif); 978 } else if (shuffle == MediaPlaybackService.SHUFFLE_NORMAL || 979 shuffle == MediaPlaybackService.SHUFFLE_AUTO) { 980 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE); 981 showToast(R.string.shuffle_off_notif); 982 } else { 983 Log.e("MediaPlaybackActivity", "Invalid shuffle mode: " + shuffle); 984 } 985 setShuffleButtonImage(); 986 } catch (RemoteException ex) { 987 } 988 } 989 990 private void cycleRepeat() { 991 if (mService == null) { 992 return; 993 } 994 try { 995 int mode = mService.getRepeatMode(); 996 if (mode == MediaPlaybackService.REPEAT_NONE) { 997 mService.setRepeatMode(MediaPlaybackService.REPEAT_ALL); 998 showToast(R.string.repeat_all_notif); 999 } else if (mode == MediaPlaybackService.REPEAT_ALL) { 1000 mService.setRepeatMode(MediaPlaybackService.REPEAT_CURRENT); 1001 if (mService.getShuffleMode() != MediaPlaybackService.SHUFFLE_NONE) { 1002 mService.setShuffleMode(MediaPlaybackService.SHUFFLE_NONE); 1003 setShuffleButtonImage(); 1004 } 1005 showToast(R.string.repeat_current_notif); 1006 } else { 1007 mService.setRepeatMode(MediaPlaybackService.REPEAT_NONE); 1008 showToast(R.string.repeat_off_notif); 1009 } 1010 setRepeatButtonImage(); 1011 } catch (RemoteException ex) { 1012 } 1013 1014 } 1015 1016 private void showToast(int resid) { 1017 if (mToast == null) { 1018 mToast = Toast.makeText(this, "", Toast.LENGTH_SHORT); 1019 } 1020 mToast.setText(resid); 1021 mToast.show(); 1022 } 1023 1024 private void startPlayback() { 1025 1026 if(mService == null) 1027 return; 1028 Intent intent = getIntent(); 1029 String filename = ""; 1030 Uri uri = intent.getData(); 1031 if (uri != null && uri.toString().length() > 0) { 1032 // If this is a file:// URI, just use the path directly instead 1033 // of going through the open-from-filedescriptor codepath. 1034 String scheme = uri.getScheme(); 1035 if ("file".equals(scheme)) { 1036 filename = uri.getPath(); 1037 } else { 1038 filename = uri.toString(); 1039 } 1040 try { 1041 mService.stop(); 1042 mService.openFile(filename); 1043 mService.play(); 1044 setIntent(new Intent()); 1045 } catch (Exception ex) { 1046 Log.d("MediaPlaybackActivity", "couldn't start playback: " + ex); 1047 } 1048 } 1049 1050 updateTrackInfo(); 1051 long next = refreshNow(); 1052 queueNextRefresh(next); 1053 } 1054 1055 private ServiceConnection osc = new ServiceConnection() { 1056 public void onServiceConnected(ComponentName classname, IBinder obj) { 1057 mService = IMediaPlaybackService.Stub.asInterface(obj); 1058 startPlayback(); 1059 try { 1060 // Assume something is playing when the service says it is, 1061 // but also if the audio ID is valid but the service is paused. 1062 if (mService.getAudioId() >= 0 || mService.isPlaying() || 1063 mService.getPath() != null) { 1064 // something is playing now, we're done 1065 mRepeatButton.setVisibility(View.VISIBLE); 1066 mShuffleButton.setVisibility(View.VISIBLE); 1067 mQueueButton.setVisibility(View.VISIBLE); 1068 setRepeatButtonImage(); 1069 setShuffleButtonImage(); 1070 setPauseButtonImage(); 1071 return; 1072 } 1073 } catch (RemoteException ex) { 1074 } 1075 // Service is dead or not playing anything. If we got here as part 1076 // of a "play this file" Intent, exit. Otherwise go to the Music 1077 // app start screen. 1078 if (getIntent().getData() == null) { 1079 Intent intent = new Intent(Intent.ACTION_MAIN); 1080 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1081 intent.setClass(MediaPlaybackActivity.this, MusicBrowserActivity.class); 1082 startActivity(intent); 1083 } 1084 finish(); 1085 } 1086 public void onServiceDisconnected(ComponentName classname) { 1087 mService = null; 1088 } 1089 }; 1090 1091 private void setRepeatButtonImage() { 1092 if (mService == null) return; 1093 try { 1094 switch (mService.getRepeatMode()) { 1095 case MediaPlaybackService.REPEAT_ALL: 1096 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_all_btn); 1097 break; 1098 case MediaPlaybackService.REPEAT_CURRENT: 1099 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_once_btn); 1100 break; 1101 default: 1102 mRepeatButton.setImageResource(R.drawable.ic_mp_repeat_off_btn); 1103 break; 1104 } 1105 } catch (RemoteException ex) { 1106 } 1107 } 1108 1109 private void setShuffleButtonImage() { 1110 if (mService == null) return; 1111 try { 1112 switch (mService.getShuffleMode()) { 1113 case MediaPlaybackService.SHUFFLE_NONE: 1114 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_off_btn); 1115 break; 1116 case MediaPlaybackService.SHUFFLE_AUTO: 1117 mShuffleButton.setImageResource(R.drawable.ic_mp_partyshuffle_on_btn); 1118 break; 1119 default: 1120 mShuffleButton.setImageResource(R.drawable.ic_mp_shuffle_on_btn); 1121 break; 1122 } 1123 } catch (RemoteException ex) { 1124 } 1125 } 1126 1127 private void setPauseButtonImage() { 1128 try { 1129 if (mService != null && mService.isPlaying()) { 1130 mPauseButton.setImageResource(android.R.drawable.ic_media_pause); 1131 } else { 1132 mPauseButton.setImageResource(android.R.drawable.ic_media_play); 1133 } 1134 } catch (RemoteException ex) { 1135 } 1136 } 1137 1138 private ImageView mAlbum; 1139 private TextView mCurrentTime; 1140 private TextView mTotalTime; 1141 private TextView mArtistName; 1142 private TextView mAlbumName; 1143 private TextView mTrackName; 1144 private ProgressBar mProgress; 1145 private long mPosOverride = -1; 1146 private boolean mFromTouch = false; 1147 private long mDuration; 1148 private int seekmethod; 1149 private boolean paused; 1150 1151 private static final int REFRESH = 1; 1152 private static final int QUIT = 2; 1153 private static final int GET_ALBUM_ART = 3; 1154 private static final int ALBUM_ART_DECODED = 4; 1155 1156 private void queueNextRefresh(long delay) { 1157 if (!paused) { 1158 Message msg = mHandler.obtainMessage(REFRESH); 1159 mHandler.removeMessages(REFRESH); 1160 mHandler.sendMessageDelayed(msg, delay); 1161 } 1162 } 1163 1164 private long refreshNow() { 1165 if(mService == null) 1166 return 500; 1167 try { 1168 long pos = mPosOverride < 0 ? mService.position() : mPosOverride; 1169 long remaining = 1000 - (pos % 1000); 1170 if ((pos >= 0) && (mDuration > 0)) { 1171 mCurrentTime.setText(MusicUtils.makeTimeString(this, pos / 1000)); 1172 1173 if (mService.isPlaying()) { 1174 mCurrentTime.setVisibility(View.VISIBLE); 1175 } else { 1176 // blink the counter 1177 int vis = mCurrentTime.getVisibility(); 1178 mCurrentTime.setVisibility(vis == View.INVISIBLE ? View.VISIBLE : View.INVISIBLE); 1179 remaining = 500; 1180 } 1181 1182 mProgress.setProgress((int) (1000 * pos / mDuration)); 1183 } else { 1184 mCurrentTime.setText("--:--"); 1185 mProgress.setProgress(1000); 1186 } 1187 // return the number of milliseconds until the next full second, so 1188 // the counter can be updated at just the right time 1189 return remaining; 1190 } catch (RemoteException ex) { 1191 } 1192 return 500; 1193 } 1194 1195 private final Handler mHandler = new Handler() { 1196 @Override 1197 public void handleMessage(Message msg) { 1198 switch (msg.what) { 1199 case ALBUM_ART_DECODED: 1200 mAlbum.setImageBitmap((Bitmap)msg.obj); 1201 mAlbum.getDrawable().setDither(true); 1202 break; 1203 1204 case REFRESH: 1205 long next = refreshNow(); 1206 queueNextRefresh(next); 1207 break; 1208 1209 case QUIT: 1210 // This can be moved back to onCreate once the bug that prevents 1211 // Dialogs from being started from onCreate/onResume is fixed. 1212 new AlertDialog.Builder(MediaPlaybackActivity.this) 1213 .setTitle(R.string.service_start_error_title) 1214 .setMessage(R.string.service_start_error_msg) 1215 .setPositiveButton(R.string.service_start_error_button, 1216 new DialogInterface.OnClickListener() { 1217 public void onClick(DialogInterface dialog, int whichButton) { 1218 finish(); 1219 } 1220 }) 1221 .setCancelable(false) 1222 .show(); 1223 break; 1224 1225 default: 1226 break; 1227 } 1228 } 1229 }; 1230 1231 private BroadcastReceiver mStatusListener = new BroadcastReceiver() { 1232 @Override 1233 public void onReceive(Context context, Intent intent) { 1234 String action = intent.getAction(); 1235 if (action.equals(MediaPlaybackService.META_CHANGED)) { 1236 // redraw the artist/title info and 1237 // set new max for progress bar 1238 updateTrackInfo(); 1239 setPauseButtonImage(); 1240 queueNextRefresh(1); 1241 } else if (action.equals(MediaPlaybackService.PLAYBACK_COMPLETE) || 1242 action.equals(MediaPlaybackService.PLAYSTATE_CHANGED)) { 1243 setPauseButtonImage(); 1244 } 1245 } 1246 }; 1247 1248 private static class AlbumSongIdWrapper { 1249 public long albumid; 1250 public long songid; 1251 AlbumSongIdWrapper(long aid, long sid) { 1252 albumid = aid; 1253 songid = sid; 1254 } 1255 } 1256 1257 private void updateTrackInfo() { 1258 if (mService == null) { 1259 return; 1260 } 1261 try { 1262 String path = mService.getPath(); 1263 if (path == null) { 1264 finish(); 1265 return; 1266 } 1267 1268 long songid = mService.getAudioId(); 1269 if (songid < 0 && path.toLowerCase().startsWith("http://")) { 1270 // Once we can get album art and meta data from MediaPlayer, we 1271 // can show that info again when streaming. 1272 ((View) mArtistName.getParent()).setVisibility(View.INVISIBLE); 1273 ((View) mAlbumName.getParent()).setVisibility(View.INVISIBLE); 1274 mAlbum.setVisibility(View.GONE); 1275 mTrackName.setText(path); 1276 mAlbumArtHandler.removeMessages(GET_ALBUM_ART); 1277 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(-1, -1)).sendToTarget(); 1278 } else { 1279 ((View) mArtistName.getParent()).setVisibility(View.VISIBLE); 1280 ((View) mAlbumName.getParent()).setVisibility(View.VISIBLE); 1281 String artistName = mService.getArtistName(); 1282 if (MediaStore.UNKNOWN_STRING.equals(artistName)) { 1283 artistName = getString(R.string.unknown_artist_name); 1284 } 1285 mArtistName.setText(artistName); 1286 String albumName = mService.getAlbumName(); 1287 long albumid = mService.getAlbumId(); 1288 if (MediaStore.UNKNOWN_STRING.equals(albumName)) { 1289 albumName = getString(R.string.unknown_album_name); 1290 albumid = -1; 1291 } 1292 mAlbumName.setText(albumName); 1293 mTrackName.setText(mService.getTrackName()); 1294 mAlbumArtHandler.removeMessages(GET_ALBUM_ART); 1295 mAlbumArtHandler.obtainMessage(GET_ALBUM_ART, new AlbumSongIdWrapper(albumid, songid)).sendToTarget(); 1296 mAlbum.setVisibility(View.VISIBLE); 1297 } 1298 mDuration = mService.duration(); 1299 mTotalTime.setText(MusicUtils.makeTimeString(this, mDuration / 1000)); 1300 } catch (RemoteException ex) { 1301 finish(); 1302 } 1303 } 1304 1305 public class AlbumArtHandler extends Handler { 1306 private long mAlbumId = -1; 1307 1308 public AlbumArtHandler(Looper looper) { 1309 super(looper); 1310 } 1311 @Override 1312 public void handleMessage(Message msg) 1313 { 1314 long albumid = ((AlbumSongIdWrapper) msg.obj).albumid; 1315 long songid = ((AlbumSongIdWrapper) msg.obj).songid; 1316 if (msg.what == GET_ALBUM_ART && (mAlbumId != albumid || albumid < 0)) { 1317 // while decoding the new image, show the default album art 1318 Message numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, null); 1319 mHandler.removeMessages(ALBUM_ART_DECODED); 1320 mHandler.sendMessageDelayed(numsg, 300); 1321 Bitmap bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, albumid); 1322 if (bm == null) { 1323 bm = MusicUtils.getArtwork(MediaPlaybackActivity.this, songid, -1); 1324 albumid = -1; 1325 } 1326 if (bm != null) { 1327 numsg = mHandler.obtainMessage(ALBUM_ART_DECODED, bm); 1328 mHandler.removeMessages(ALBUM_ART_DECODED); 1329 mHandler.sendMessage(numsg); 1330 } 1331 mAlbumId = albumid; 1332 } 1333 } 1334 } 1335 1336 private static class Worker implements Runnable { 1337 private final Object mLock = new Object(); 1338 private Looper mLooper; 1339 1340 /** 1341 * Creates a worker thread with the given name. The thread 1342 * then runs a {@link android.os.Looper}. 1343 * @param name A name for the new thread 1344 */ 1345 Worker(String name) { 1346 Thread t = new Thread(null, this, name); 1347 t.setPriority(Thread.MIN_PRIORITY); 1348 t.start(); 1349 synchronized (mLock) { 1350 while (mLooper == null) { 1351 try { 1352 mLock.wait(); 1353 } catch (InterruptedException ex) { 1354 } 1355 } 1356 } 1357 } 1358 1359 public Looper getLooper() { 1360 return mLooper; 1361 } 1362 1363 public void run() { 1364 synchronized (mLock) { 1365 Looper.prepare(); 1366 mLooper = Looper.myLooper(); 1367 mLock.notifyAll(); 1368 } 1369 Looper.loop(); 1370 } 1371 1372 public void quit() { 1373 mLooper.quit(); 1374 } 1375 } 1376} 1377 1378