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