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 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 MediaController(Context context, AttributeSet attrs)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 onFinishInflate()106 public void onFinishInflate() { 107 if (mRoot != null) 108 initControllerView(mRoot); 109 } 110 MediaController(Context context, boolean useFastForward)111 public MediaController(Context context, boolean useFastForward) { 112 super(context); 113 mContext = context; 114 mUseFastForward = useFastForward; 115 initFloatingWindow(); 116 } 117 MediaController(Context context)118 public MediaController(Context context) { 119 super(context); 120 mContext = context; 121 mUseFastForward = true; 122 initFloatingWindow(); 123 } 124 initFloatingWindow()125 private void initFloatingWindow() { 126 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 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 setMediaPlayer(MediaPlayerControl player)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 */ setAnchorView(View view)166 public void setAnchorView(View view) { 167 mAnchor = view; 168 169 FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( 170 ViewGroup.LayoutParams.MATCH_PARENT, 171 ViewGroup.LayoutParams.MATCH_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 */ makeControllerView()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 initControllerView(View v)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 */ show()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 */ disableUnsupportedButtons()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 */ show(int timeout)281 public void show(int timeout) { 282 283 if (!mShowing && mAnchor != null) { 284 setProgress(); 285 if (mPauseButton != null) { 286 mPauseButton.requestFocus(); 287 } 288 disableUnsupportedButtons(); 289 290 int [] anchorpos = new int[2]; 291 mAnchor.getLocationOnScreen(anchorpos); 292 293 WindowManager.LayoutParams p = new WindowManager.LayoutParams(); 294 p.gravity = Gravity.TOP; 295 p.width = mAnchor.getWidth(); 296 p.height = LayoutParams.WRAP_CONTENT; 297 p.x = 0; 298 p.y = anchorpos[1] + mAnchor.getHeight() - p.height; 299 p.format = PixelFormat.TRANSLUCENT; 300 p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; 301 p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 302 p.token = null; 303 p.windowAnimations = 0; // android.R.style.DropDownAnimationDown; 304 mWindowManager.addView(mDecor, p); 305 mShowing = true; 306 } 307 updatePausePlay(); 308 309 // cause the progress bar to be updated even if mShowing 310 // was already true. This happens, for example, if we're 311 // paused with the progress bar showing the user hits play. 312 mHandler.sendEmptyMessage(SHOW_PROGRESS); 313 314 Message msg = mHandler.obtainMessage(FADE_OUT); 315 if (timeout != 0) { 316 mHandler.removeMessages(FADE_OUT); 317 mHandler.sendMessageDelayed(msg, timeout); 318 } 319 } 320 isShowing()321 public boolean isShowing() { 322 return mShowing; 323 } 324 325 /** 326 * Remove the controller from the screen. 327 */ hide()328 public void hide() { 329 if (mAnchor == null) 330 return; 331 332 if (mShowing) { 333 try { 334 mHandler.removeMessages(SHOW_PROGRESS); 335 mWindowManager.removeView(mDecor); 336 } catch (IllegalArgumentException ex) { 337 Log.w("MediaController", "already removed"); 338 } 339 mShowing = false; 340 } 341 } 342 343 private Handler mHandler = new Handler() { 344 @Override 345 public void handleMessage(Message msg) { 346 int pos; 347 switch (msg.what) { 348 case FADE_OUT: 349 hide(); 350 break; 351 case SHOW_PROGRESS: 352 pos = setProgress(); 353 if (!mDragging && mShowing && mPlayer.isPlaying()) { 354 msg = obtainMessage(SHOW_PROGRESS); 355 sendMessageDelayed(msg, 1000 - (pos % 1000)); 356 } 357 break; 358 } 359 } 360 }; 361 stringForTime(int timeMs)362 private String stringForTime(int timeMs) { 363 int totalSeconds = timeMs / 1000; 364 365 int seconds = totalSeconds % 60; 366 int minutes = (totalSeconds / 60) % 60; 367 int hours = totalSeconds / 3600; 368 369 mFormatBuilder.setLength(0); 370 if (hours > 0) { 371 return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString(); 372 } else { 373 return mFormatter.format("%02d:%02d", minutes, seconds).toString(); 374 } 375 } 376 setProgress()377 private int setProgress() { 378 if (mPlayer == null || mDragging) { 379 return 0; 380 } 381 int position = mPlayer.getCurrentPosition(); 382 int duration = mPlayer.getDuration(); 383 if (mProgress != null) { 384 if (duration > 0) { 385 // use long to avoid overflow 386 long pos = 1000L * position / duration; 387 mProgress.setProgress( (int) pos); 388 } 389 int percent = mPlayer.getBufferPercentage(); 390 mProgress.setSecondaryProgress(percent * 10); 391 } 392 393 if (mEndTime != null) 394 mEndTime.setText(stringForTime(duration)); 395 if (mCurrentTime != null) 396 mCurrentTime.setText(stringForTime(position)); 397 398 return position; 399 } 400 401 @Override onTouchEvent(MotionEvent event)402 public boolean onTouchEvent(MotionEvent event) { 403 show(sDefaultTimeout); 404 return true; 405 } 406 407 @Override onTrackballEvent(MotionEvent ev)408 public boolean onTrackballEvent(MotionEvent ev) { 409 show(sDefaultTimeout); 410 return false; 411 } 412 413 @Override dispatchKeyEvent(KeyEvent event)414 public boolean dispatchKeyEvent(KeyEvent event) { 415 int keyCode = event.getKeyCode(); 416 if (event.getRepeatCount() == 0 && event.isDown() && ( 417 keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 418 keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || 419 keyCode == KeyEvent.KEYCODE_SPACE)) { 420 doPauseResume(); 421 show(sDefaultTimeout); 422 if (mPauseButton != null) { 423 mPauseButton.requestFocus(); 424 } 425 return true; 426 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP) { 427 if (mPlayer.isPlaying()) { 428 mPlayer.pause(); 429 updatePausePlay(); 430 } 431 return true; 432 } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 433 keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 434 // don't show the controls for volume adjustment 435 return super.dispatchKeyEvent(event); 436 } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) { 437 hide(); 438 439 return true; 440 } else { 441 show(sDefaultTimeout); 442 } 443 return super.dispatchKeyEvent(event); 444 } 445 446 private View.OnClickListener mPauseListener = new View.OnClickListener() { 447 public void onClick(View v) { 448 doPauseResume(); 449 show(sDefaultTimeout); 450 } 451 }; 452 updatePausePlay()453 private void updatePausePlay() { 454 if (mRoot == null || mPauseButton == null) 455 return; 456 457 if (mPlayer.isPlaying()) { 458 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause); 459 } else { 460 mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play); 461 } 462 } 463 doPauseResume()464 private void doPauseResume() { 465 if (mPlayer.isPlaying()) { 466 mPlayer.pause(); 467 } else { 468 mPlayer.start(); 469 } 470 updatePausePlay(); 471 } 472 473 // There are two scenarios that can trigger the seekbar listener to trigger: 474 // 475 // The first is the user using the touchpad to adjust the posititon of the 476 // seekbar's thumb. In this case onStartTrackingTouch is called followed by 477 // a number of onProgressChanged notifications, concluded by onStopTrackingTouch. 478 // We're setting the field "mDragging" to true for the duration of the dragging 479 // session to avoid jumps in the position in case of ongoing playback. 480 // 481 // The second scenario involves the user operating the scroll ball, in this 482 // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications, 483 // we will simply apply the updated position without suspending regular updates. 484 private OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() { 485 public void onStartTrackingTouch(SeekBar bar) { 486 show(3600000); 487 488 mDragging = true; 489 490 // By removing these pending progress messages we make sure 491 // that a) we won't update the progress while the user adjusts 492 // the seekbar and b) once the user is done dragging the thumb 493 // we will post one of these messages to the queue again and 494 // this ensures that there will be exactly one message queued up. 495 mHandler.removeMessages(SHOW_PROGRESS); 496 } 497 498 public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) { 499 if (!fromuser) { 500 // We're not interested in programmatically generated changes to 501 // the progress bar's position. 502 return; 503 } 504 505 long duration = mPlayer.getDuration(); 506 long newposition = (duration * progress) / 1000L; 507 mPlayer.seekTo( (int) newposition); 508 if (mCurrentTime != null) 509 mCurrentTime.setText(stringForTime( (int) newposition)); 510 } 511 512 public void onStopTrackingTouch(SeekBar bar) { 513 mDragging = false; 514 setProgress(); 515 updatePausePlay(); 516 show(sDefaultTimeout); 517 518 // Ensure that progress is properly updated in the future, 519 // the call to show() does not guarantee this because it is a 520 // no-op if we are already showing. 521 mHandler.sendEmptyMessage(SHOW_PROGRESS); 522 } 523 }; 524 525 @Override setEnabled(boolean enabled)526 public void setEnabled(boolean enabled) { 527 if (mPauseButton != null) { 528 mPauseButton.setEnabled(enabled); 529 } 530 if (mFfwdButton != null) { 531 mFfwdButton.setEnabled(enabled); 532 } 533 if (mRewButton != null) { 534 mRewButton.setEnabled(enabled); 535 } 536 if (mNextButton != null) { 537 mNextButton.setEnabled(enabled && mNextListener != null); 538 } 539 if (mPrevButton != null) { 540 mPrevButton.setEnabled(enabled && mPrevListener != null); 541 } 542 if (mProgress != null) { 543 mProgress.setEnabled(enabled); 544 } 545 disableUnsupportedButtons(); 546 super.setEnabled(enabled); 547 } 548 549 private View.OnClickListener mRewListener = new View.OnClickListener() { 550 public void onClick(View v) { 551 int pos = mPlayer.getCurrentPosition(); 552 pos -= 5000; // milliseconds 553 mPlayer.seekTo(pos); 554 setProgress(); 555 556 show(sDefaultTimeout); 557 } 558 }; 559 560 private View.OnClickListener mFfwdListener = new View.OnClickListener() { 561 public void onClick(View v) { 562 int pos = mPlayer.getCurrentPosition(); 563 pos += 15000; // milliseconds 564 mPlayer.seekTo(pos); 565 setProgress(); 566 567 show(sDefaultTimeout); 568 } 569 }; 570 installPrevNextListeners()571 private void installPrevNextListeners() { 572 if (mNextButton != null) { 573 mNextButton.setOnClickListener(mNextListener); 574 mNextButton.setEnabled(mNextListener != null); 575 } 576 577 if (mPrevButton != null) { 578 mPrevButton.setOnClickListener(mPrevListener); 579 mPrevButton.setEnabled(mPrevListener != null); 580 } 581 } 582 setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev)583 public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) { 584 mNextListener = next; 585 mPrevListener = prev; 586 mListenersSet = true; 587 588 if (mRoot != null) { 589 installPrevNextListeners(); 590 591 if (mNextButton != null && !mFromXml) { 592 mNextButton.setVisibility(View.VISIBLE); 593 } 594 if (mPrevButton != null && !mFromXml) { 595 mPrevButton.setVisibility(View.VISIBLE); 596 } 597 } 598 } 599 600 public interface MediaPlayerControl { start()601 void start(); pause()602 void pause(); getDuration()603 int getDuration(); getCurrentPosition()604 int getCurrentPosition(); seekTo(int pos)605 void seekTo(int pos); isPlaying()606 boolean isPlaying(); getBufferPercentage()607 int getBufferPercentage(); canPause()608 boolean canPause(); canSeekBackward()609 boolean canSeekBackward(); canSeekForward()610 boolean canSeekForward(); 611 } 612 } 613