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 import android.window.WindowOnBackInvokedDispatcher; 42 43 import com.android.internal.policy.PhoneWindow; 44 45 import java.util.Formatter; 46 import java.util.Locale; 47 48 /** 49 * A view containing controls for a MediaPlayer. Typically contains the 50 * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress 51 * slider. It takes care of synchronizing the controls with the state 52 * of the MediaPlayer. 53 * <p> 54 * The way to use this class is to instantiate it programmatically. 55 * The MediaController will create a default set of controls 56 * and put them in a window floating above your application. Specifically, 57 * the controls will float above the view specified with setAnchorView(). 58 * The window will disappear if left idle for three seconds and reappear 59 * when the user touches the anchor view. 60 * <p> 61 * Functions like show() and hide() have no effect when MediaController 62 * is created in an xml layout. 63 * 64 * MediaController will hide and 65 * show the buttons according to these rules: 66 * <ul> 67 * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners() 68 * has been called 69 * <li> The "previous" and "next" buttons are visible but disabled if 70 * setPrevNextListeners() was called with null listeners 71 * <li> The "rewind" and "fastforward" buttons are shown unless requested 72 * otherwise by using the MediaController(Context, boolean) constructor 73 * with the boolean set to false 74 * </ul> 75 */ 76 public class MediaController extends FrameLayout { 77 78 @UnsupportedAppUsage 79 private MediaPlayerControl mPlayer; 80 @UnsupportedAppUsage 81 private final Context mContext; 82 @UnsupportedAppUsage 83 private View mAnchor; 84 @UnsupportedAppUsage 85 private View mRoot; 86 @UnsupportedAppUsage 87 private WindowManager mWindowManager; 88 @UnsupportedAppUsage 89 private Window mWindow; 90 @UnsupportedAppUsage 91 private View mDecor; 92 @UnsupportedAppUsage 93 private WindowManager.LayoutParams mDecorLayoutParams; 94 @UnsupportedAppUsage 95 private ProgressBar mProgress; 96 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 97 private TextView mEndTime; 98 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 99 private TextView mCurrentTime; 100 @UnsupportedAppUsage 101 private boolean mShowing; 102 private boolean mDragging; 103 private static final int sDefaultTimeout = 3000; 104 private final boolean mUseFastForward; 105 private boolean mFromXml; 106 private boolean mListenersSet; 107 private View.OnClickListener mNextListener, mPrevListener; 108 StringBuilder mFormatBuilder; 109 Formatter mFormatter; 110 @UnsupportedAppUsage 111 private ImageButton mPauseButton; 112 @UnsupportedAppUsage 113 private ImageButton mFfwdButton; 114 @UnsupportedAppUsage 115 private ImageButton mRewButton; 116 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 117 private ImageButton mNextButton; 118 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 119 private ImageButton mPrevButton; 120 private CharSequence mPlayDescription; 121 private CharSequence mPauseDescription; 122 private final AccessibilityManager mAccessibilityManager; 123 private boolean mBackCallbackRegistered; 124 /** Handles back invocation */ 125 private final OnBackInvokedCallback mBackCallback = this::hide; 126 /** Handles decor view attach state change */ 127 private final OnAttachStateChangeListener mAttachStateListener = 128 new OnAttachStateChangeListener() { 129 @Override 130 public void onViewAttachedToWindow(@NonNull View v) { 131 registerOnBackInvokedCallback(); 132 } 133 134 @Override 135 public void onViewDetachedFromWindow(@NonNull View v) { 136 unregisterOnBackInvokedCallback(); 137 } 138 }; 139 MediaController(Context context, AttributeSet attrs)140 public MediaController(Context context, AttributeSet attrs) { 141 super(context, attrs); 142 mRoot = this; 143 mContext = context; 144 mUseFastForward = true; 145 mFromXml = true; 146 mAccessibilityManager = AccessibilityManager.getInstance(context); 147 } 148 149 @Override onFinishInflate()150 public void onFinishInflate() { 151 if (mRoot != null) 152 initControllerView(mRoot); 153 } 154 MediaController(Context context, boolean useFastForward)155 public MediaController(Context context, boolean useFastForward) { 156 super(context); 157 mContext = context; 158 mUseFastForward = useFastForward; 159 initFloatingWindowLayout(); 160 initFloatingWindow(); 161 mAccessibilityManager = AccessibilityManager.getInstance(context); 162 } 163 MediaController(Context context)164 public MediaController(Context context) { 165 this(context, true); 166 } 167 initFloatingWindow()168 private void initFloatingWindow() { 169 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 170 mWindow = new PhoneWindow(mContext); 171 mWindow.setWindowManager(mWindowManager, null, null); 172 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 173 mDecor = mWindow.getDecorView(); 174 mDecor.setOnTouchListener(mTouchListener); 175 mDecor.addOnAttachStateChangeListener(mAttachStateListener); 176 mWindow.setContentView(this); 177 mWindow.setBackgroundDrawableResource(android.R.color.transparent); 178 179 // While the media controller is up, the volume control keys should 180 // affect the media stream type 181 mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC); 182 183 setFocusable(true); 184 setFocusableInTouchMode(true); 185 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 186 requestFocus(); 187 } 188 189 // Allocate and initialize the static parts of mDecorLayoutParams. Must 190 // also call updateFloatingWindowLayout() to fill in the dynamic parts 191 // (y and width) before mDecorLayoutParams can be used. initFloatingWindowLayout()192 private void initFloatingWindowLayout() { 193 mDecorLayoutParams = new WindowManager.LayoutParams(); 194 WindowManager.LayoutParams p = mDecorLayoutParams; 195 p.gravity = Gravity.TOP | Gravity.LEFT; 196 p.height = LayoutParams.WRAP_CONTENT; 197 p.x = 0; 198 p.format = PixelFormat.TRANSLUCENT; 199 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 200 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 201 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 202 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; 203 p.token = null; 204 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; 205 } 206 207 // Update the dynamic parts of mDecorLayoutParams 208 // Must be called with mAnchor != NULL. updateFloatingWindowLayout()209 private void updateFloatingWindowLayout() { 210 int [] anchorPos = new int[2]; 211 mAnchor.getLocationOnScreen(anchorPos); 212 213 // we need to know the size of the controller so we can properly position it 214 // within its space 215 mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), 216 MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); 217 218 WindowManager.LayoutParams p = mDecorLayoutParams; 219 p.width = mAnchor.getWidth(); 220 p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; 221 p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); 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 && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled( 752 viewRootImpl.mContext)) { 753 viewRootImpl.getOnBackInvokedDispatcher() 754 .unregisterOnBackInvokedCallback(mBackCallback); 755 } 756 mBackCallbackRegistered = false; 757 } 758 registerOnBackInvokedCallback()759 private void registerOnBackInvokedCallback() { 760 if (mBackCallbackRegistered) { 761 return; 762 } 763 764 ViewRootImpl viewRootImpl = mDecor.getViewRootImpl(); 765 if (viewRootImpl != null 766 && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled( 767 viewRootImpl.mContext)) { 768 viewRootImpl.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 769 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackCallback); 770 mBackCallbackRegistered = true; 771 } 772 } 773 774 public interface MediaPlayerControl { start()775 void start(); pause()776 void pause(); getDuration()777 int getDuration(); getCurrentPosition()778 int getCurrentPosition(); seekTo(int pos)779 void seekTo(int pos); isPlaying()780 boolean isPlaying(); getBufferPercentage()781 int getBufferPercentage(); canPause()782 boolean canPause(); canSeekBackward()783 boolean canSeekBackward(); canSeekForward()784 boolean canSeekForward(); 785 786 /** 787 * Get the audio session id for the player used by this VideoView. This can be used to 788 * apply audio effects to the audio track of a video. 789 * @return The audio session, or 0 if there was an error. 790 */ getAudioSessionId()791 int getAudioSessionId(); 792 } 793 } 794