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