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 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 109 public void onFinishInflate() { 110 if (mRoot != null) 111 initControllerView(mRoot); 112 } 113 114 public MediaController(Context context, boolean useFastForward) { 115 super(context); 116 mContext = context; 117 mUseFastForward = useFastForward; 118 initFloatingWindowLayout(); 119 initFloatingWindow(); 120 } 121 122 public MediaController(Context context) { 123 this(context, true); 124 } 125 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. 149 private void initFloatingWindowLayout() { 150 mDecorLayoutParams = new WindowManager.LayoutParams(); 151 WindowManager.LayoutParams p = mDecorLayoutParams; 152 p.gravity = Gravity.TOP | Gravity.LEFT; 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. 166 private void updateFloatingWindowLayout() { 167 int [] anchorPos = new int[2]; 168 mAnchor.getLocationOnScreen(anchorPos); 169 170 // we need to know the size of the controller so we can properly position it 171 // within its space 172 mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST), 173 MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST)); 174 175 WindowManager.LayoutParams p = mDecorLayoutParams; 176 p.width = mAnchor.getWidth(); 177 p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; 178 p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); 179 } 180 181 // This is called whenever mAnchor's layout bound changes 182 private OnLayoutChangeListener mLayoutChangeListener = 183 new OnLayoutChangeListener() { 184 public void onLayoutChange(View v, int left, int top, int right, 185 int bottom, int oldLeft, int oldTop, int oldRight, 186 int oldBottom) { 187 updateFloatingWindowLayout(); 188 if (mShowing) { 189 mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams); 190 } 191 } 192 }; 193 194 private OnTouchListener mTouchListener = new OnTouchListener() { 195 public boolean onTouch(View v, MotionEvent event) { 196 if (event.getAction() == MotionEvent.ACTION_DOWN) { 197 if (mShowing) { 198 hide(); 199 } 200 } 201 return false; 202 } 203 }; 204 205 public void setMediaPlayer(MediaPlayerControl player) { 206 mPlayer = player; 207 updatePausePlay(); 208 } 209 210 /** 211 * Set the view that acts as the anchor for the control view. 212 * This can for example be a VideoView, or your Activity's main view. 213 * When VideoView calls this method, it will use the VideoView's parent 214 * as the anchor. 215 * @param view The view to which to anchor the controller when it is visible. 216 */ 217 public void setAnchorView(View view) { 218 if (mAnchor != null) { 219 mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener); 220 } 221 mAnchor = view; 222 if (mAnchor != null) { 223 mAnchor.addOnLayoutChangeListener(mLayoutChangeListener); 224 } 225 226 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( 227 ViewGroup.LayoutParams.MATCH_PARENT, 228 ViewGroup.LayoutParams.MATCH_PARENT 229 ); 230 231 removeAllViews(); 232 View v = makeControllerView(); 233 addView(v, frameParams); 234 } 235 236 /** 237 * Create the view that holds the widgets that control playback. 238 * Derived classes can override this to create their own. 239 * @return The controller view. 240 * @hide This doesn't work as advertised 241 */ 242 protected View makeControllerView() { 243 LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 244 mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null); 245 246 initControllerView(mRoot); 247 248 return mRoot; 249 } 250 251 private void initControllerView(View v) { 252 mPauseButton = (ImageButton) v.findViewById(com.android.internal.R.id.pause); 253 if (mPauseButton != null) { 254 mPauseButton.requestFocus(); 255 mPauseButton.setOnClickListener(mPauseListener); 256 } 257 258 mFfwdButton = (ImageButton) v.findViewById(com.android.internal.R.id.ffwd); 259 if (mFfwdButton != null) { 260 mFfwdButton.setOnClickListener(mFfwdListener); 261 if (!mFromXml) { 262 mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 263 } 264 } 265 266 mRewButton = (ImageButton) v.findViewById(com.android.internal.R.id.rew); 267 if (mRewButton != null) { 268 mRewButton.setOnClickListener(mRewListener); 269 if (!mFromXml) { 270 mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE); 271 } 272 } 273 274 // By default these are hidden. They will be enabled when setPrevNextListeners() is called 275 mNextButton = (ImageButton) v.findViewById(com.android.internal.R.id.next); 276 if (mNextButton != null && !mFromXml && !mListenersSet) { 277 mNextButton.setVisibility(View.GONE); 278 } 279 mPrevButton = (ImageButton) v.findViewById(com.android.internal.R.id.prev); 280 if (mPrevButton != null && !mFromXml && !mListenersSet) { 281 mPrevButton.setVisibility(View.GONE); 282 } 283 284 mProgress = (ProgressBar) v.findViewById(com.android.internal.R.id.mediacontroller_progress); 285 if (mProgress != null) { 286 if (mProgress instanceof SeekBar) { 287 SeekBar seeker = (SeekBar) mProgress; 288 seeker.setOnSeekBarChangeListener(mSeekListener); 289 } 290 mProgress.setMax(1000); 291 } 292 293 mEndTime = (TextView) v.findViewById(com.android.internal.R.id.time); 294 mCurrentTime = (TextView) v.findViewById(com.android.internal.R.id.time_current); 295 mFormatBuilder = new StringBuilder(); 296 mFormatter = new Formatter(mFormatBuilder, Locale.getDefault()); 297 298 installPrevNextListeners(); 299 } 300 301 /** 302 * Show the controller on screen. It will go away 303 * automatically after 3 seconds of inactivity. 304 */ 305 public void show() { 306 show(sDefaultTimeout); 307 } 308 309 /** 310 * Disable pause or seek buttons if the stream cannot be paused or seeked. 311 * This requires the control interface to be a MediaPlayerControlExt 312 */ 313 private void disableUnsupportedButtons() { 314 try { 315 if (mPauseButton != null && !mPlayer.canPause()) { 316 mPauseButton.setEnabled(false); 317 } 318 if (mRewButton != null && !mPlayer.canSeekBackward()) { 319 mRewButton.setEnabled(false); 320 } 321 if (mFfwdButton != null && !mPlayer.canSeekForward()) { 322 mFfwdButton.setEnabled(false); 323 } 324 } catch (IncompatibleClassChangeError ex) { 325 // We were given an old version of the interface, that doesn't have 326 // the canPause/canSeekXYZ methods. This is OK, it just means we 327 // assume the media can be paused and seeked, and so we don't disable 328 // the buttons. 329 } 330 } 331 332 /** 333 * Show the controller on screen. It will go away 334 * automatically after 'timeout' milliseconds of inactivity. 335 * @param timeout The timeout in milliseconds. Use 0 to show 336 * the controller until hide() is called. 337 */ 338 public void show(int timeout) { 339 if (!mShowing && mAnchor != null) { 340 setProgress(); 341 if (mPauseButton != null) { 342 mPauseButton.requestFocus(); 343 } 344 disableUnsupportedButtons(); 345 updateFloatingWindowLayout(); 346 mWindowManager.addView(mDecor, mDecorLayoutParams); 347 mShowing = true; 348 } 349 updatePausePlay(); 350 351 // cause the progress bar to be updated even if mShowing 352 // was already true. This happens, for example, if we're 353 // paused with the progress bar showing the user hits play. 354 mHandler.sendEmptyMessage(SHOW_PROGRESS); 355 356 Message msg = mHandler.obtainMessage(FADE_OUT); 357 if (timeout != 0) { 358 mHandler.removeMessages(FADE_OUT); 359 mHandler.sendMessageDelayed(msg, timeout); 360 } 361 } 362 363 public boolean isShowing() { 364 return mShowing; 365 } 366 367 /** 368 * Remove the controller from the screen. 369 */ 370 public void hide() { 371 if (mAnchor == null) 372 return; 373 374 if (mShowing) { 375 try { 376 mHandler.removeMessages(SHOW_PROGRESS); 377 mWindowManager.removeView(mDecor); 378 } catch (IllegalArgumentException ex) { 379 Log.w("MediaController", "already removed"); 380 } 381 mShowing = false; 382 } 383 } 384 385 private Handler mHandler = new Handler() { 386 @Override 387 public void handleMessage(Message msg) { 388 int pos; 389 switch (msg.what) { 390 case FADE_OUT: 391 hide(); 392 break; 393 case SHOW_PROGRESS: 394 pos = setProgress(); 395 if (!mDragging && mShowing && mPlayer.isPlaying()) { 396 msg = obtainMessage(SHOW_PROGRESS); 397 sendMessageDelayed(msg, 1000 - (pos % 1000)); 398 } 399 break; 400 } 401 } 402 }; 403 404 private String stringForTime(int timeMs) { 405 int totalSeconds = timeMs / 1000; 406 407 int seconds = totalSeconds % 60; 408 int minutes = (totalSeconds / 60) % 60; 409 int hours = totalSeconds / 3600; 410 411 mFormatBuilder.setLength(0); 412 if (hours > 0) { 413 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 414 } else { 415 return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 416 } 417 } 418 419 private int setProgress() { 420 if (mPlayer == null || mDragging) { 421 return 0; 422 } 423 int position = mPlayer.getCurrentPosition(); 424 int duration = mPlayer.getDuration(); 425 if (mProgress != null) { 426 if (duration > 0) { 427 // use long to avoid overflow 428 long pos = 1000L * position / duration; 429 mProgress.setProgress( (int) pos); 430 } 431 int percent = mPlayer.getBufferPercentage(); 432 mProgress.setSecondaryProgress(percent * 10); 433 } 434 435 if (mEndTime != null) 436 mEndTime.setText(stringForTime(duration)); 437 if (mCurrentTime != null) 438 mCurrentTime.setText(stringForTime(position)); 439 440 return position; 441 } 442 443 @Override 444 public boolean onTouchEvent(MotionEvent event) { 445 show(sDefaultTimeout); 446 return true; 447 } 448 449 @Override 450 public boolean onTrackballEvent(MotionEvent ev) { 451 show(sDefaultTimeout); 452 return false; 453 } 454 455 @Override 456 public boolean dispatchKeyEvent(KeyEvent event) { 457 int keyCode = event.getKeyCode(); 458 final boolean uniqueDown = event.getRepeatCount() == 0 459 && event.getAction() == KeyEvent.ACTION_DOWN; 460 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK 461 || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE 462 || keyCode == KeyEvent.KEYCODE_SPACE) { 463 if (uniqueDown) { 464 doPauseResume(); 465 show(sDefaultTimeout); 466 if (mPauseButton != null) { 467 mPauseButton.requestFocus(); 468 } 469 } 470 return true; 471 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 472 if (uniqueDown && !mPlayer.isPlaying()) { 473 mPlayer.start(); 474 updatePausePlay(); 475 show(sDefaultTimeout); 476 } 477 return true; 478 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 479 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 480 if (uniqueDown && mPlayer.isPlaying()) { 481 mPlayer.pause(); 482 updatePausePlay(); 483 show(sDefaultTimeout); 484 } 485 return true; 486 } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 487 || keyCode == KeyEvent.KEYCODE_VOLUME_UP 488 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE 489 || keyCode == KeyEvent.KEYCODE_CAMERA) { 490 // don't show the controls for volume adjustment 491 return super.dispatchKeyEvent(event); 492 } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { 493 if (uniqueDown) { 494 hide(); 495 } 496 return true; 497 } 498 499 show(sDefaultTimeout); 500 return super.dispatchKeyEvent(event); 501 } 502 503 private View.OnClickListener mPauseListener = new View.OnClickListener() { 504 public void onClick(View v) { 505 doPauseResume(); 506 show(sDefaultTimeout); 507 } 508 }; 509 510 private void updatePausePlay() { 511 if (mRoot == null || mPauseButton == null) 512 return; 513 514 if (mPlayer.isPlaying()) { 515 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause); 516 } else { 517 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play); 518 } 519 } 520 521 private void doPauseResume() { 522 if (mPlayer.isPlaying()) { 523 mPlayer.pause(); 524 } else { 525 mPlayer.start(); 526 } 527 updatePausePlay(); 528 } 529 530 // There are two scenarios that can trigger the seekbar listener to trigger: 531 // 532 // The first is the user using the touchpad to adjust the posititon of the 533 // seekbar's thumb. In this case onStartTrackingTouch is called followed by 534 // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. 535 // We're setting the field "mDragging" to true for the duration of the dragging 536 // session to avoid jumps in the position in case of ongoing playback. 537 // 538 // The second scenario involves the user operating the scroll ball, in this 539 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, 540 // we will simply apply the updated position without suspending regular updates. 541 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 542 public void onStartTrackingTouch(SeekBar bar) { 543 show(3600000); 544 545 mDragging = true; 546 547 // By removing these pending progress messages we make sure 548 // that a) we won't update the progress while the user adjusts 549 // the seekbar and b) once the user is done dragging the thumb 550 // we will post one of these messages to the queue again and 551 // this ensures that there will be exactly one message queued up. 552 mHandler.removeMessages(SHOW_PROGRESS); 553 } 554 555 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 556 if (!fromuser) { 557 // We're not interested in programmatically generated changes to 558 // the progress bar's position. 559 return; 560 } 561 562 long duration = mPlayer.getDuration(); 563 long newposition = (duration * progress) / 1000L; 564 mPlayer.seekTo( (int) newposition); 565 if (mCurrentTime != null) 566 mCurrentTime.setText(stringForTime( (int) newposition)); 567 } 568 569 public void onStopTrackingTouch(SeekBar bar) { 570 mDragging = false; 571 setProgress(); 572 updatePausePlay(); 573 show(sDefaultTimeout); 574 575 // Ensure that progress is properly updated in the future, 576 // the call to show() does not guarantee this because it is a 577 // no-op if we are already showing. 578 mHandler.sendEmptyMessage(SHOW_PROGRESS); 579 } 580 }; 581 582 @Override 583 public void setEnabled(boolean enabled) { 584 if (mPauseButton != null) { 585 mPauseButton.setEnabled(enabled); 586 } 587 if (mFfwdButton != null) { 588 mFfwdButton.setEnabled(enabled); 589 } 590 if (mRewButton != null) { 591 mRewButton.setEnabled(enabled); 592 } 593 if (mNextButton != null) { 594 mNextButton.setEnabled(enabled && mNextListener != null); 595 } 596 if (mPrevButton != null) { 597 mPrevButton.setEnabled(enabled && mPrevListener != null); 598 } 599 if (mProgress != null) { 600 mProgress.setEnabled(enabled); 601 } 602 disableUnsupportedButtons(); 603 super.setEnabled(enabled); 604 } 605 606 @Override 607 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 608 super.onInitializeAccessibilityEvent(event); 609 event.setClassName(MediaController.class.getName()); 610 } 611 612 @Override 613 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 614 super.onInitializeAccessibilityNodeInfo(info); 615 info.setClassName(MediaController.class.getName()); 616 } 617 618 private View.OnClickListener mRewListener = new View.OnClickListener() { 619 public void onClick(View v) { 620 int pos = mPlayer.getCurrentPosition(); 621 pos -= 5000; // milliseconds 622 mPlayer.seekTo(pos); 623 setProgress(); 624 625 show(sDefaultTimeout); 626 } 627 }; 628 629 private View.OnClickListener mFfwdListener = new View.OnClickListener() { 630 public void onClick(View v) { 631 int pos = mPlayer.getCurrentPosition(); 632 pos += 15000; // milliseconds 633 mPlayer.seekTo(pos); 634 setProgress(); 635 636 show(sDefaultTimeout); 637 } 638 }; 639 640 private void installPrevNextListeners() { 641 if (mNextButton != null) { 642 mNextButton.setOnClickListener(mNextListener); 643 mNextButton.setEnabled(mNextListener != null); 644 } 645 646 if (mPrevButton != null) { 647 mPrevButton.setOnClickListener(mPrevListener); 648 mPrevButton.setEnabled(mPrevListener != null); 649 } 650 } 651 652 public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { 653 mNextListener = next; 654 mPrevListener = prev; 655 mListenersSet = true; 656 657 if (mRoot != null) { 658 installPrevNextListeners(); 659 660 if (mNextButton != null && !mFromXml) { 661 mNextButton.setVisibility(View.VISIBLE); 662 } 663 if (mPrevButton != null && !mFromXml) { 664 mPrevButton.setVisibility(View.VISIBLE); 665 } 666 } 667 } 668 669 public interface MediaPlayerControl { 670 void start(); 671 void pause(); 672 int getDuration(); 673 int getCurrentPosition(); 674 void seekTo(int pos); 675 boolean isPlaying(); 676 int getBufferPercentage(); 677 boolean canPause(); 678 boolean canSeekBackward(); 679 boolean canSeekForward(); 680 681 /** 682 * Get the audio session id for the player used by this VideoView. This can be used to 683 * apply audio effects to the audio track of a video. 684 * @return The audio session, or 0 if there was an error. 685 */ 686 int getAudioSessionId(); 687 } 688 } 689