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