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