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