1 /* 2 * Copyright (C) 2006 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 17 package android.widget; 18 19 import android.content.Context; 20 import android.graphics.PixelFormat; 21 import android.media.AudioManager; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.util.AttributeSet; 25 import android.util.Log; 26 import android.view.Gravity; 27 import android.view.KeyEvent; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.Window; 33 import android.view.WindowManager; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.accessibility.AccessibilityNodeInfo; 36 import android.widget.SeekBar.OnSeekBarChangeListener; 37 38 import com.android.internal.policy.PolicyManager; 39 40 import java.util.Formatter; 41 import java.util.Locale; 42 43 /** 44 * A view containing controls for a MediaPlayer. Typically contains the 45 * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress 46 * slider. It takes care of synchronizing the controls with the state 47 * of the MediaPlayer. 48 * <p> 49 * The way to use this class is to instantiate it programatically. 50 * The MediaController will create a default set of controls 51 * and put them in a window floating above your application. Specifically, 52 * the controls will float above the view specified with setAnchorView(). 53 * The window will disappear if left idle for three seconds and reappear 54 * when the user touches the anchor view. 55 * <p> 56 * Functions like show() and hide() have no effect when MediaController 57 * is created in an xml layout. 58 * 59 * MediaController will hide and 60 * show the buttons according to these rules: 61 * <ul> 62 * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners() 63 * has been called 64 * <li> The "previous" and "next" buttons are visible but disabled if 65 * setPrevNextListeners() was called with null listeners 66 * <li> The "rewind" and "fastforward" buttons are shown unless requested 67 * otherwise by using the MediaController(Context, boolean) constructor 68 * with the boolean set to false 69 * </ul> 70 */ 71 public class MediaController extends FrameLayout { 72 73 private MediaPlayerControl mPlayer; 74 private Context mContext; 75 private View mAnchor; 76 private View mRoot; 77 private WindowManager mWindowManager; 78 private Window mWindow; 79 private View mDecor; 80 private WindowManager.LayoutParams mDecorLayoutParams; 81 private ProgressBar mProgress; 82 private TextView mEndTime, mCurrentTime; 83 private boolean mShowing; 84 private boolean mDragging; 85 private static final int sDefaultTimeout = 3000; 86 private static final int FADE_OUT = 1; 87 private static final int SHOW_PROGRESS = 2; 88 private boolean mUseFastForward; 89 private boolean mFromXml; 90 private boolean mListenersSet; 91 private View.OnClickListener mNextListener, mPrevListener; 92 StringBuilder mFormatBuilder; 93 Formatter mFormatter; 94 private ImageButton mPauseButton; 95 private ImageButton mFfwdButton; 96 private ImageButton mRewButton; 97 private ImageButton mNextButton; 98 private ImageButton mPrevButton; 99 MediaController(Context context, AttributeSet attrs)100 public MediaController(Context context, AttributeSet attrs) { 101 super(context, attrs); 102 mRoot = this; 103 mContext = context; 104 mUseFastForward = true; 105 mFromXml = true; 106 } 107 108 @Override onFinishInflate()109 public void onFinishInflate() { 110 if (mRoot != null) 111 initControllerView(mRoot); 112 } 113 MediaController(Context context, boolean useFastForward)114 public MediaController(Context context, boolean useFastForward) { 115 super(context); 116 mContext = context; 117 mUseFastForward = useFastForward; 118 initFloatingWindowLayout(); 119 initFloatingWindow(); 120 } 121 MediaController(Context context)122 public MediaController(Context context) { 123 this(context, true); 124 } 125 initFloatingWindow()126 private void initFloatingWindow() { 127 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 128 mWindow = PolicyManager.makeNewWindow(mContext); 129 mWindow.setWindowManager(mWindowManager, null, null); 130 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 131 mDecor = mWindow.getDecorView(); 132 mDecor.setOnTouchListener(mTouchListener); 133 mWindow.setContentView(this); 134 mWindow.setBackgroundDrawableResource(android.R.color.transparent); 135 136 // While the media controller is up, the volume control keys should 137 // affect the media stream type 138 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); 139 140 setFocusable(true); 141 setFocusableInTouchMode(true); 142 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 143 requestFocus(); 144 } 145 146 // Allocate and initialize the static parts of mDecorLayoutParams. Must 147 // also call updateFloatingWindowLayout() to fill in the dynamic parts 148 // (y and width) before mDecorLayoutParams can be used. initFloatingWindowLayout()149 private void initFloatingWindowLayout() { 150 mDecorLayoutParams = new WindowManager.LayoutParams(); 151 WindowManager.LayoutParams p = mDecorLayoutParams; 152 p.gravity = Gravity.TOP; 153 p.height = LayoutParams.WRAP_CONTENT; 154 p.x = 0; 155 p.format = PixelFormat.TRANSLUCENT; 156 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 157 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 158 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 159 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; 160 p.token = null; 161 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; 162 } 163 164 // Update the dynamic parts of mDecorLayoutParams 165 // Must be called with mAnchor != NULL. updateFloatingWindowLayout()166 private void updateFloatingWindowLayout() { 167 int [] anchorPos = new int[2]; 168 mAnchor.getLocationOnScreen(anchorPos); 169 170 WindowManager.LayoutParams p = mDecorLayoutParams; 171 p.width = mAnchor.getWidth(); 172 p.y = anchorPos[1] + mAnchor.getHeight(); 173 } 174 175 // This is called whenever mAnchor's layout bound changes 176 private OnLayoutChangeListener mLayoutChangeListener = 177 new OnLayoutChangeListener() { 178 public void onLayoutChange(View v, int left, int top, int right, 179 int bottom, int oldLeft, int oldTop, int oldRight, 180 int oldBottom) { 181 updateFloatingWindowLayout(); 182 if (mShowing) { 183 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); 184 } 185 } 186 }; 187 188 private OnTouchListener mTouchListener = new OnTouchListener() { 189 public boolean onTouch(View v, MotionEvent event) { 190 if (event.getAction() == MotionEvent.ACTION_DOWN) { 191 if (mShowing) { 192 hide(); 193 } 194 } 195 return false; 196 } 197 }; 198 setMediaPlayer(MediaPlayerControl player)199 public void setMediaPlayer(MediaPlayerControl player) { 200 mPlayer = player; 201 updatePausePlay(); 202 } 203 204 /** 205 * Set the view that acts as the anchor for the control view. 206 * This can for example be a VideoView, or your Activity's main view. 207 * @param view The view to which to anchor the controller when it is visible. 208 */ setAnchorView(View view)209 public void setAnchorView(View view) { 210 if (mAnchor != null) { 211 mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); 212 } 213 mAnchor = view; 214 if (mAnchor != null) { 215 mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); 216 } 217 218 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( 219 ViewGroup.LayoutParams.MATCH_PARENT, 220 ViewGroup.LayoutParams.MATCH_PARENT 221 ); 222 223 removeAllViews(); 224 View v = makeControllerView(); 225 addView(v, frameParams); 226 } 227 228 /** 229 * Create the view that holds the widgets that control playback. 230 * Derived classes can override this to create their own. 231 * @return The controller view. 232 * @hide This doesn't work as advertised 233 */ makeControllerView()234 protected View makeControllerView() { 235 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 236 mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); 237 238 initControllerView(mRoot); 239 240 return mRoot; 241 } 242 initControllerView(View v)243 private void initControllerView(View v) { 244 mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause); 245 if (mPauseButton != null) { 246 mPauseButton.requestFocus(); 247 mPauseButton.setOnClickListener(mPauseListener); 248 } 249 250 mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd); 251 if (mFfwdButton != null) { 252 mFfwdButton.setOnClickListener(mFfwdListener); 253 if (!mFromXml) { 254 mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 255 } 256 } 257 258 mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew); 259 if (mRewButton != null) { 260 mRewButton.setOnClickListener(mRewListener); 261 if (!mFromXml) { 262 mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 263 } 264 } 265 266 // By default these are hidden. They will be enabled when setPrevNextListeners() is called 267 mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next); 268 if (mNextButton != null && !mFromXml && !mListenersSet) { 269 mNextButton.setVisibility(View.GONE); 270 } 271 mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev); 272 if (mPrevButton != null && !mFromXml && !mListenersSet) { 273 mPrevButton.setVisibility(View.GONE); 274 } 275 276 mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress); 277 if (mProgress != null) { 278 if (mProgress instanceof SeekBar) { 279 SeekBar seeker = (SeekBar) mProgress; 280 seeker.setOnSeekBarChangeListener(mSeekListener); 281 } 282 mProgress.setMax(1000); 283 } 284 285 mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time); 286 mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current); 287 mFormatBuilder = new StringBuilder(); 288 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); 289 290 installPrevNextListeners(); 291 } 292 293 /** 294 * Show the controller on screen. It will go away 295 * automatically after 3 seconds of inactivity. 296 */ show()297 public void show() { 298 show(sDefaultTimeout); 299 } 300 301 /** 302 * Disable pause or seek buttons if the stream cannot be paused or seeked. 303 * This requires the control interface to be a MediaPlayerControlExt 304 */ disableUnsupportedButtons()305 private void disableUnsupportedButtons() { 306 try { 307 if (mPauseButton != null && !mPlayer.canPause()) { 308 mPauseButton.setEnabled(false); 309 } 310 if (mRewButton != null && !mPlayer.canSeekBackward()) { 311 mRewButton.setEnabled(false); 312 } 313 if (mFfwdButton != null && !mPlayer.canSeekForward()) { 314 mFfwdButton.setEnabled(false); 315 } 316 } catch (IncompatibleClassChangeError ex) { 317 // We were given an old version of the interface, that doesn't have 318 // the canPause/canSeekXYZ methods. This is OK, it just means we 319 // assume the media can be paused and seeked, and so we don't disable 320 // the buttons. 321 } 322 } 323 324 /** 325 * Show the controller on screen. It will go away 326 * automatically after 'timeout' milliseconds of inactivity. 327 * @param timeout The timeout in milliseconds. Use 0 to show 328 * the controller until hide() is called. 329 */ show(int timeout)330 public void show(int timeout) { 331 if (!mShowing && mAnchor != null) { 332 setProgress(); 333 if (mPauseButton != null) { 334 mPauseButton.requestFocus(); 335 } 336 disableUnsupportedButtons(); 337 updateFloatingWindowLayout(); 338 mWindowManager.addView(mDecor, mDecorLayoutParams); 339 mShowing = true; 340 } 341 updatePausePlay(); 342 343 // cause the progress bar to be updated even if mShowing 344 // was already true. This happens, for example, if we're 345 // paused with the progress bar showing the user hits play. 346 mHandler.sendEmptyMessage(SHOW_PROGRESS); 347 348 Message msg = mHandler.obtainMessage(FADE_OUT); 349 if (timeout != 0) { 350 mHandler.removeMessages(FADE_OUT); 351 mHandler.sendMessageDelayed(msg, timeout); 352 } 353 } 354 isShowing()355 public boolean isShowing() { 356 return mShowing; 357 } 358 359 /** 360 * Remove the controller from the screen. 361 */ hide()362 public void hide() { 363 if (mAnchor == null) 364 return; 365 366 if (mShowing) { 367 try { 368 mHandler.removeMessages(SHOW_PROGRESS); 369 mWindowManager.removeView(mDecor); 370 } catch (IllegalArgumentException ex) { 371 Log.w("MediaController", "already removed"); 372 } 373 mShowing = false; 374 } 375 } 376 377 private Handler mHandler = new Handler() { 378 @Override 379 public void handleMessage(Message msg) { 380 int pos; 381 switch (msg.what) { 382 case FADE_OUT: 383 hide(); 384 break; 385 case SHOW_PROGRESS: 386 pos = setProgress(); 387 if (!mDragging && mShowing && mPlayer.isPlaying()) { 388 msg = obtainMessage(SHOW_PROGRESS); 389 sendMessageDelayed(msg, 1000 - (pos % 1000)); 390 } 391 break; 392 } 393 } 394 }; 395 stringForTime(int timeMs)396 private String stringForTime(int timeMs) { 397 int totalSeconds = timeMs / 1000; 398 399 int seconds = totalSeconds % 60; 400 int minutes = (totalSeconds / 60) % 60; 401 int hours = totalSeconds / 3600; 402 403 mFormatBuilder.setLength(0); 404 if (hours > 0) { 405 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 406 } else { 407 return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 408 } 409 } 410 setProgress()411 private int setProgress() { 412 if (mPlayer == null || mDragging) { 413 return 0; 414 } 415 int position = mPlayer.getCurrentPosition(); 416 int duration = mPlayer.getDuration(); 417 if (mProgress != null) { 418 if (duration > 0) { 419 // use long to avoid overflow 420 long pos = 1000L * position / duration; 421 mProgress.setProgress( (int) pos); 422 } 423 int percent = mPlayer.getBufferPercentage(); 424 mProgress.setSecondaryProgress(percent * 10); 425 } 426 427 if (mEndTime != null) 428 mEndTime.setText(stringForTime(duration)); 429 if (mCurrentTime != null) 430 mCurrentTime.setText(stringForTime(position)); 431 432 return position; 433 } 434 435 @Override onTouchEvent(MotionEvent event)436 public boolean onTouchEvent(MotionEvent event) { 437 show(sDefaultTimeout); 438 return true; 439 } 440 441 @Override onTrackballEvent(MotionEvent ev)442 public boolean onTrackballEvent(MotionEvent ev) { 443 show(sDefaultTimeout); 444 return false; 445 } 446 447 @Override dispatchKeyEvent(KeyEvent event)448 public boolean dispatchKeyEvent(KeyEvent event) { 449 int keyCode = event.getKeyCode(); 450 final boolean uniqueDown = event.getRepeatCount() == 0 451 && event.getAction() == KeyEvent.ACTION_DOWN; 452 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK 453 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 454 || keyCode == KeyEvent.KEYCODE_SPACE) { 455 if (uniqueDown) { 456 doPauseResume(); 457 show(sDefaultTimeout); 458 if (mPauseButton != null) { 459 mPauseButton.requestFocus(); 460 } 461 } 462 return true; 463 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 464 if (uniqueDown && !mPlayer.isPlaying()) { 465 mPlayer.start(); 466 updatePausePlay(); 467 show(sDefaultTimeout); 468 } 469 return true; 470 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 471 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 472 if (uniqueDown && mPlayer.isPlaying()) { 473 mPlayer.pause(); 474 updatePausePlay(); 475 show(sDefaultTimeout); 476 } 477 return true; 478 } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 479 || keyCode == KeyEvent.KEYCODE_VOLUME_UP 480 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE 481 || keyCode == KeyEvent.KEYCODE_CAMERA) { 482 // don't show the controls for volume adjustment 483 return super.dispatchKeyEvent(event); 484 } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { 485 if (uniqueDown) { 486 hide(); 487 } 488 return true; 489 } 490 491 show(sDefaultTimeout); 492 return super.dispatchKeyEvent(event); 493 } 494 495 private View.OnClickListener mPauseListener = new View.OnClickListener() { 496 public void onClick(View v) { 497 doPauseResume(); 498 show(sDefaultTimeout); 499 } 500 }; 501 updatePausePlay()502 private void updatePausePlay() { 503 if (mRoot == null || mPauseButton == null) 504 return; 505 506 if (mPlayer.isPlaying()) { 507 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause); 508 } else { 509 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play); 510 } 511 } 512 doPauseResume()513 private void doPauseResume() { 514 if (mPlayer.isPlaying()) { 515 mPlayer.pause(); 516 } else { 517 mPlayer.start(); 518 } 519 updatePausePlay(); 520 } 521 522 // There are two scenarios that can trigger the seekbar listener to trigger: 523 // 524 // The first is the user using the touchpad to adjust the posititon of the 525 // seekbar's thumb. In this case onStartTrackingTouch is called followed by 526 // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. 527 // We're setting the field "mDragging" to true for the duration of the dragging 528 // session to avoid jumps in the position in case of ongoing playback. 529 // 530 // The second scenario involves the user operating the scroll ball, in this 531 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, 532 // we will simply apply the updated position without suspending regular updates. 533 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 534 public void onStartTrackingTouch(SeekBar bar) { 535 show(3600000); 536 537 mDragging = true; 538 539 // By removing these pending progress messages we make sure 540 // that a) we won't update the progress while the user adjusts 541 // the seekbar and b) once the user is done dragging the thumb 542 // we will post one of these messages to the queue again and 543 // this ensures that there will be exactly one message queued up. 544 mHandler.removeMessages(SHOW_PROGRESS); 545 } 546 547 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 548 if (!fromuser) { 549 // We're not interested in programmatically generated changes to 550 // the progress bar's position. 551 return; 552 } 553 554 long duration = mPlayer.getDuration(); 555 long newposition = (duration * progress) / 1000L; 556 mPlayer.seekTo( (int) newposition); 557 if (mCurrentTime != null) 558 mCurrentTime.setText(stringForTime( (int) newposition)); 559 } 560 561 public void onStopTrackingTouch(SeekBar bar) { 562 mDragging = false; 563 setProgress(); 564 updatePausePlay(); 565 show(sDefaultTimeout); 566 567 // Ensure that progress is properly updated in the future, 568 // the call to show() does not guarantee this because it is a 569 // no-op if we are already showing. 570 mHandler.sendEmptyMessage(SHOW_PROGRESS); 571 } 572 }; 573 574 @Override setEnabled(boolean enabled)575 public void setEnabled(boolean enabled) { 576 if (mPauseButton != null) { 577 mPauseButton.setEnabled(enabled); 578 } 579 if (mFfwdButton != null) { 580 mFfwdButton.setEnabled(enabled); 581 } 582 if (mRewButton != null) { 583 mRewButton.setEnabled(enabled); 584 } 585 if (mNextButton != null) { 586 mNextButton.setEnabled(enabled && mNextListener != null); 587 } 588 if (mPrevButton != null) { 589 mPrevButton.setEnabled(enabled && mPrevListener != null); 590 } 591 if (mProgress != null) { 592 mProgress.setEnabled(enabled); 593 } 594 disableUnsupportedButtons(); 595 super.setEnabled(enabled); 596 } 597 598 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)599 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 600 super.onInitializeAccessibilityEvent(event); 601 event.setClassName(MediaController.class.getName()); 602 } 603 604 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)605 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 606 super.onInitializeAccessibilityNodeInfo(info); 607 info.setClassName(MediaController.class.getName()); 608 } 609 610 private View.OnClickListener mRewListener = new View.OnClickListener() { 611 public void onClick(View v) { 612 int pos = mPlayer.getCurrentPosition(); 613 pos -= 5000; // milliseconds 614 mPlayer.seekTo(pos); 615 setProgress(); 616 617 show(sDefaultTimeout); 618 } 619 }; 620 621 private View.OnClickListener mFfwdListener = new View.OnClickListener() { 622 public void onClick(View v) { 623 int pos = mPlayer.getCurrentPosition(); 624 pos += 15000; // milliseconds 625 mPlayer.seekTo(pos); 626 setProgress(); 627 628 show(sDefaultTimeout); 629 } 630 }; 631 installPrevNextListeners()632 private void installPrevNextListeners() { 633 if (mNextButton != null) { 634 mNextButton.setOnClickListener(mNextListener); 635 mNextButton.setEnabled(mNextListener != null); 636 } 637 638 if (mPrevButton != null) { 639 mPrevButton.setOnClickListener(mPrevListener); 640 mPrevButton.setEnabled(mPrevListener != null); 641 } 642 } 643 setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev)644 public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { 645 mNextListener = next; 646 mPrevListener = prev; 647 mListenersSet = true; 648 649 if (mRoot != null) { 650 installPrevNextListeners(); 651 652 if (mNextButton != null && !mFromXml) { 653 mNextButton.setVisibility(View.VISIBLE); 654 } 655 if (mPrevButton != null && !mFromXml) { 656 mPrevButton.setVisibility(View.VISIBLE); 657 } 658 } 659 } 660 661 public interface MediaPlayerControl { start()662 void start(); pause()663 void pause(); getDuration()664 int getDuration(); getCurrentPosition()665 int getCurrentPosition(); seekTo(int pos)666 void seekTo(int pos); isPlaying()667 boolean isPlaying(); getBufferPercentage()668 int getBufferPercentage(); canPause()669 boolean canPause(); canSeekBackward()670 boolean canSeekBackward(); canSeekForward()671 boolean canSeekForward(); 672 } 673 } 674