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.app.AlertDialog; 20import android.content.Context; 21import android.content.DialogInterface; 22import android.content.Intent; 23import android.content.res.Resources; 24import android.graphics.Canvas; 25import android.media.AudioManager; 26import android.media.MediaFormat; 27import android.media.MediaPlayer; 28import android.media.MediaPlayer.OnCompletionListener; 29import android.media.MediaPlayer.OnErrorListener; 30import android.media.MediaPlayer.OnInfoListener; 31import android.media.Metadata; 32import android.media.SubtitleController; 33import android.media.SubtitleTrack.RenderingWidget; 34import android.media.WebVttRenderer; 35import android.net.Uri; 36import android.os.Looper; 37import android.util.AttributeSet; 38import android.util.Log; 39import android.util.Pair; 40import android.view.KeyEvent; 41import android.view.MotionEvent; 42import android.view.SurfaceHolder; 43import android.view.SurfaceView; 44import android.view.View; 45import android.view.accessibility.AccessibilityEvent; 46import android.view.accessibility.AccessibilityNodeInfo; 47import android.widget.MediaController.MediaPlayerControl; 48 49import java.io.IOException; 50import java.io.InputStream; 51import java.util.Map; 52import java.util.Vector; 53 54/** 55 * Displays a video file. The VideoView class 56 * can load images from various sources (such as resources or content 57 * providers), takes care of computing its measurement from the video so that 58 * it can be used in any layout manager, and provides various display options 59 * such as scaling and tinting. 60 */ 61public class VideoView extends SurfaceView 62 implements MediaPlayerControl, SubtitleController.Anchor { 63 private String TAG = "VideoView"; 64 // settable by the client 65 private Uri mUri; 66 private Map<String, String> mHeaders; 67 68 // all possible internal states 69 private static final int STATE_ERROR = -1; 70 private static final int STATE_IDLE = 0; 71 private static final int STATE_PREPARING = 1; 72 private static final int STATE_PREPARED = 2; 73 private static final int STATE_PLAYING = 3; 74 private static final int STATE_PAUSED = 4; 75 private static final int STATE_PLAYBACK_COMPLETED = 5; 76 77 // mCurrentState is a VideoView object's current state. 78 // mTargetState is the state that a method caller intends to reach. 79 // For instance, regardless the VideoView object's current state, 80 // calling pause() intends to bring the object to a target state 81 // of STATE_PAUSED. 82 private int mCurrentState = STATE_IDLE; 83 private int mTargetState = STATE_IDLE; 84 85 // All the stuff we need for playing and showing a video 86 private SurfaceHolder mSurfaceHolder = null; 87 private MediaPlayer mMediaPlayer = null; 88 private int mAudioSession; 89 private int mVideoWidth; 90 private int mVideoHeight; 91 private int mSurfaceWidth; 92 private int mSurfaceHeight; 93 private MediaController mMediaController; 94 private OnCompletionListener mOnCompletionListener; 95 private MediaPlayer.OnPreparedListener mOnPreparedListener; 96 private int mCurrentBufferPercentage; 97 private OnErrorListener mOnErrorListener; 98 private OnInfoListener mOnInfoListener; 99 private int mSeekWhenPrepared; // recording the seek position while preparing 100 private boolean mCanPause; 101 private boolean mCanSeekBack; 102 private boolean mCanSeekForward; 103 104 /** Subtitle rendering widget overlaid on top of the video. */ 105 private RenderingWidget mSubtitleWidget; 106 107 /** Listener for changes to subtitle data, used to redraw when needed. */ 108 private RenderingWidget.OnChangedListener mSubtitlesChangedListener; 109 110 public VideoView(Context context) { 111 super(context); 112 initVideoView(); 113 } 114 115 public VideoView(Context context, AttributeSet attrs) { 116 this(context, attrs, 0); 117 initVideoView(); 118 } 119 120 public VideoView(Context context, AttributeSet attrs, int defStyle) { 121 super(context, attrs, defStyle); 122 initVideoView(); 123 } 124 125 @Override 126 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 127 //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", " 128 // + MeasureSpec.toString(heightMeasureSpec) + ")"); 129 130 int width = getDefaultSize(mVideoWidth, widthMeasureSpec); 131 int height = getDefaultSize(mVideoHeight, heightMeasureSpec); 132 if (mVideoWidth > 0 && mVideoHeight > 0) { 133 134 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 135 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 136 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 137 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 138 139 if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { 140 // the size is fixed 141 width = widthSpecSize; 142 height = heightSpecSize; 143 144 // for compatibility, we adjust size based on aspect ratio 145 if ( mVideoWidth * height < width * mVideoHeight ) { 146 //Log.i("@@@", "image too wide, correcting"); 147 width = height * mVideoWidth / mVideoHeight; 148 } else if ( mVideoWidth * height > width * mVideoHeight ) { 149 //Log.i("@@@", "image too tall, correcting"); 150 height = width * mVideoHeight / mVideoWidth; 151 } 152 } else if (widthSpecMode == MeasureSpec.EXACTLY) { 153 // only the width is fixed, adjust the height to match aspect ratio if possible 154 width = widthSpecSize; 155 height = width * mVideoHeight / mVideoWidth; 156 if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 157 // couldn't match aspect ratio within the constraints 158 height = heightSpecSize; 159 } 160 } else if (heightSpecMode == MeasureSpec.EXACTLY) { 161 // only the height is fixed, adjust the width to match aspect ratio if possible 162 height = heightSpecSize; 163 width = height * mVideoWidth / mVideoHeight; 164 if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 165 // couldn't match aspect ratio within the constraints 166 width = widthSpecSize; 167 } 168 } else { 169 // neither the width nor the height are fixed, try to use actual video size 170 width = mVideoWidth; 171 height = mVideoHeight; 172 if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) { 173 // too tall, decrease both width and height 174 height = heightSpecSize; 175 width = height * mVideoWidth / mVideoHeight; 176 } 177 if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) { 178 // too wide, decrease both width and height 179 width = widthSpecSize; 180 height = width * mVideoHeight / mVideoWidth; 181 } 182 } 183 } else { 184 // no size yet, just adopt the given spec sizes 185 } 186 setMeasuredDimension(width, height); 187 } 188 189 @Override 190 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 191 super.onInitializeAccessibilityEvent(event); 192 event.setClassName(VideoView.class.getName()); 193 } 194 195 @Override 196 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 197 super.onInitializeAccessibilityNodeInfo(info); 198 info.setClassName(VideoView.class.getName()); 199 } 200 201 public int resolveAdjustedSize(int desiredSize, int measureSpec) { 202 return getDefaultSize(desiredSize, measureSpec); 203 } 204 205 private void initVideoView() { 206 mVideoWidth = 0; 207 mVideoHeight = 0; 208 getHolder().addCallback(mSHCallback); 209 getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); 210 setFocusable(true); 211 setFocusableInTouchMode(true); 212 requestFocus(); 213 mPendingSubtitleTracks = new Vector<Pair<InputStream, MediaFormat>>(); 214 mCurrentState = STATE_IDLE; 215 mTargetState = STATE_IDLE; 216 } 217 218 public void setVideoPath(String path) { 219 setVideoURI(Uri.parse(path)); 220 } 221 222 public void setVideoURI(Uri uri) { 223 setVideoURI(uri, null); 224 } 225 226 /** 227 * @hide 228 */ 229 public void setVideoURI(Uri uri, Map<String, String> headers) { 230 mUri = uri; 231 mHeaders = headers; 232 mSeekWhenPrepared = 0; 233 openVideo(); 234 requestLayout(); 235 invalidate(); 236 } 237 238 /** 239 * Adds an external subtitle source file (from the provided input stream.) 240 * 241 * Note that a single external subtitle source may contain multiple or no 242 * supported tracks in it. If the source contained at least one track in 243 * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE} 244 * info message. Otherwise, if reading the source takes excessive time, 245 * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT} 246 * message. If the source contained no supported track (including an empty 247 * source file or null input stream), one will receive a {@link 248 * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the 249 * total number of available tracks using {@link MediaPlayer#getTrackInfo()} 250 * to see what additional tracks become available after this method call. 251 * 252 * @param is input stream containing the subtitle data. It will be 253 * closed by the media framework. 254 * @param format the format of the subtitle track(s). Must contain at least 255 * the mime type ({@link MediaFormat#KEY_MIME}) and the 256 * language ({@link MediaFormat#KEY_LANGUAGE}) of the file. 257 * If the file itself contains the language information, 258 * specify "und" for the language. 259 */ 260 public void addSubtitleSource(InputStream is, MediaFormat format) { 261 if (mMediaPlayer == null) { 262 mPendingSubtitleTracks.add(Pair.create(is, format)); 263 } else { 264 try { 265 mMediaPlayer.addSubtitleSource(is, format); 266 } catch (IllegalStateException e) { 267 mInfoListener.onInfo( 268 mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); 269 } 270 } 271 } 272 273 private Vector<Pair<InputStream, MediaFormat>> mPendingSubtitleTracks; 274 275 public void stopPlayback() { 276 if (mMediaPlayer != null) { 277 mMediaPlayer.stop(); 278 mMediaPlayer.release(); 279 mMediaPlayer = null; 280 mCurrentState = STATE_IDLE; 281 mTargetState = STATE_IDLE; 282 } 283 } 284 285 private void openVideo() { 286 if (mUri == null || mSurfaceHolder == null) { 287 // not ready for playback just yet, will try again later 288 return; 289 } 290 // Tell the music playback service to pause 291 // TODO: these constants need to be published somewhere in the framework. 292 Intent i = new Intent("com.android.music.musicservicecommand"); 293 i.putExtra("command", "pause"); 294 mContext.sendBroadcast(i); 295 296 // we shouldn't clear the target state, because somebody might have 297 // called start() previously 298 release(false); 299 try { 300 mMediaPlayer = new MediaPlayer(); 301 // TODO: create SubtitleController in MediaPlayer, but we need 302 // a context for the subtitle renderers 303 final Context context = getContext(); 304 final SubtitleController controller = new SubtitleController( 305 context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); 306 controller.registerRenderer(new WebVttRenderer(context)); 307 mMediaPlayer.setSubtitleAnchor(controller, this); 308 309 if (mAudioSession != 0) { 310 mMediaPlayer.setAudioSessionId(mAudioSession); 311 } else { 312 mAudioSession = mMediaPlayer.getAudioSessionId(); 313 } 314 mMediaPlayer.setOnPreparedListener(mPreparedListener); 315 mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); 316 mMediaPlayer.setOnCompletionListener(mCompletionListener); 317 mMediaPlayer.setOnErrorListener(mErrorListener); 318 mMediaPlayer.setOnInfoListener(mInfoListener); 319 mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); 320 mCurrentBufferPercentage = 0; 321 mMediaPlayer.setDataSource(mContext, mUri, mHeaders); 322 mMediaPlayer.setDisplay(mSurfaceHolder); 323 mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 324 mMediaPlayer.setScreenOnWhilePlaying(true); 325 mMediaPlayer.prepareAsync(); 326 327 for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) { 328 try { 329 mMediaPlayer.addSubtitleSource(pending.first, pending.second); 330 } catch (IllegalStateException e) { 331 mInfoListener.onInfo( 332 mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); 333 } 334 } 335 336 // we don't set the target state here either, but preserve the 337 // target state that was there before. 338 mCurrentState = STATE_PREPARING; 339 attachMediaController(); 340 } catch (IOException ex) { 341 Log.w(TAG, "Unable to open content: " + mUri, ex); 342 mCurrentState = STATE_ERROR; 343 mTargetState = STATE_ERROR; 344 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 345 return; 346 } catch (IllegalArgumentException ex) { 347 Log.w(TAG, "Unable to open content: " + mUri, ex); 348 mCurrentState = STATE_ERROR; 349 mTargetState = STATE_ERROR; 350 mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); 351 return; 352 } finally { 353 mPendingSubtitleTracks.clear(); 354 } 355 } 356 357 public void setMediaController(MediaController controller) { 358 if (mMediaController != null) { 359 mMediaController.hide(); 360 } 361 mMediaController = controller; 362 attachMediaController(); 363 } 364 365 private void attachMediaController() { 366 if (mMediaPlayer != null && mMediaController != null) { 367 mMediaController.setMediaPlayer(this); 368 View anchorView = this.getParent() instanceof View ? 369 (View)this.getParent() : this; 370 mMediaController.setAnchorView(anchorView); 371 mMediaController.setEnabled(isInPlaybackState()); 372 } 373 } 374 375 MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = 376 new MediaPlayer.OnVideoSizeChangedListener() { 377 public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { 378 mVideoWidth = mp.getVideoWidth(); 379 mVideoHeight = mp.getVideoHeight(); 380 if (mVideoWidth != 0 && mVideoHeight != 0) { 381 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 382 requestLayout(); 383 } 384 } 385 }; 386 387 MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() { 388 public void onPrepared(MediaPlayer mp) { 389 mCurrentState = STATE_PREPARED; 390 391 // Get the capabilities of the player for this stream 392 Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, 393 MediaPlayer.BYPASS_METADATA_FILTER); 394 395 if (data != null) { 396 mCanPause = !data.has(Metadata.PAUSE_AVAILABLE) 397 || data.getBoolean(Metadata.PAUSE_AVAILABLE); 398 mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE) 399 || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE); 400 mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE) 401 || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE); 402 } else { 403 mCanPause = mCanSeekBack = mCanSeekForward = true; 404 } 405 406 if (mOnPreparedListener != null) { 407 mOnPreparedListener.onPrepared(mMediaPlayer); 408 } 409 if (mMediaController != null) { 410 mMediaController.setEnabled(true); 411 } 412 mVideoWidth = mp.getVideoWidth(); 413 mVideoHeight = mp.getVideoHeight(); 414 415 int seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call 416 if (seekToPosition != 0) { 417 seekTo(seekToPosition); 418 } 419 if (mVideoWidth != 0 && mVideoHeight != 0) { 420 //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight); 421 getHolder().setFixedSize(mVideoWidth, mVideoHeight); 422 if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) { 423 // We didn't actually change the size (it was already at the size 424 // we need), so we won't get a "surface changed" callback, so 425 // start the video here instead of in the callback. 426 if (mTargetState == STATE_PLAYING) { 427 start(); 428 if (mMediaController != null) { 429 mMediaController.show(); 430 } 431 } else if (!isPlaying() && 432 (seekToPosition != 0 || getCurrentPosition() > 0)) { 433 if (mMediaController != null) { 434 // Show the media controls when we're paused into a video and make 'em stick. 435 mMediaController.show(0); 436 } 437 } 438 } 439 } else { 440 // We don't know the video size yet, but should start anyway. 441 // The video size might be reported to us later. 442 if (mTargetState == STATE_PLAYING) { 443 start(); 444 } 445 } 446 } 447 }; 448 449 private MediaPlayer.OnCompletionListener mCompletionListener = 450 new MediaPlayer.OnCompletionListener() { 451 public void onCompletion(MediaPlayer mp) { 452 mCurrentState = STATE_PLAYBACK_COMPLETED; 453 mTargetState = STATE_PLAYBACK_COMPLETED; 454 if (mMediaController != null) { 455 mMediaController.hide(); 456 } 457 if (mOnCompletionListener != null) { 458 mOnCompletionListener.onCompletion(mMediaPlayer); 459 } 460 } 461 }; 462 463 private MediaPlayer.OnInfoListener mInfoListener = 464 new MediaPlayer.OnInfoListener() { 465 public boolean onInfo(MediaPlayer mp, int arg1, int arg2) { 466 if (mOnInfoListener != null) { 467 mOnInfoListener.onInfo(mp, arg1, arg2); 468 } 469 return true; 470 } 471 }; 472 473 private MediaPlayer.OnErrorListener mErrorListener = 474 new MediaPlayer.OnErrorListener() { 475 public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { 476 Log.d(TAG, "Error: " + framework_err + "," + impl_err); 477 mCurrentState = STATE_ERROR; 478 mTargetState = STATE_ERROR; 479 if (mMediaController != null) { 480 mMediaController.hide(); 481 } 482 483 /* If an error handler has been supplied, use it and finish. */ 484 if (mOnErrorListener != null) { 485 if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) { 486 return true; 487 } 488 } 489 490 /* Otherwise, pop up an error dialog so the user knows that 491 * something bad has happened. Only try and pop up the dialog 492 * if we're attached to a window. When we're going away and no 493 * longer have a window, don't bother showing the user an error. 494 */ 495 if (getWindowToken() != null) { 496 Resources r = mContext.getResources(); 497 int messageId; 498 499 if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { 500 messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback; 501 } else { 502 messageId = com.android.internal.R.string.VideoView_error_text_unknown; 503 } 504 505 new AlertDialog.Builder(mContext) 506 .setMessage(messageId) 507 .setPositiveButton(com.android.internal.R.string.VideoView_error_button, 508 new DialogInterface.OnClickListener() { 509 public void onClick(DialogInterface dialog, int whichButton) { 510 /* If we get here, there is no onError listener, so 511 * at least inform them that the video is over. 512 */ 513 if (mOnCompletionListener != null) { 514 mOnCompletionListener.onCompletion(mMediaPlayer); 515 } 516 } 517 }) 518 .setCancelable(false) 519 .show(); 520 } 521 return true; 522 } 523 }; 524 525 private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener = 526 new MediaPlayer.OnBufferingUpdateListener() { 527 public void onBufferingUpdate(MediaPlayer mp, int percent) { 528 mCurrentBufferPercentage = percent; 529 } 530 }; 531 532 /** 533 * Register a callback to be invoked when the media file 534 * is loaded and ready to go. 535 * 536 * @param l The callback that will be run 537 */ 538 public void setOnPreparedListener(MediaPlayer.OnPreparedListener l) 539 { 540 mOnPreparedListener = l; 541 } 542 543 /** 544 * Register a callback to be invoked when the end of a media file 545 * has been reached during playback. 546 * 547 * @param l The callback that will be run 548 */ 549 public void setOnCompletionListener(OnCompletionListener l) 550 { 551 mOnCompletionListener = l; 552 } 553 554 /** 555 * Register a callback to be invoked when an error occurs 556 * during playback or setup. If no listener is specified, 557 * or if the listener returned false, VideoView will inform 558 * the user of any errors. 559 * 560 * @param l The callback that will be run 561 */ 562 public void setOnErrorListener(OnErrorListener l) 563 { 564 mOnErrorListener = l; 565 } 566 567 /** 568 * Register a callback to be invoked when an informational event 569 * occurs during playback or setup. 570 * 571 * @param l The callback that will be run 572 */ 573 public void setOnInfoListener(OnInfoListener l) { 574 mOnInfoListener = l; 575 } 576 577 SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback() 578 { 579 public void surfaceChanged(SurfaceHolder holder, int format, 580 int w, int h) 581 { 582 mSurfaceWidth = w; 583 mSurfaceHeight = h; 584 boolean isValidState = (mTargetState == STATE_PLAYING); 585 boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h); 586 if (mMediaPlayer != null && isValidState && hasValidSize) { 587 if (mSeekWhenPrepared != 0) { 588 seekTo(mSeekWhenPrepared); 589 } 590 start(); 591 } 592 } 593 594 public void surfaceCreated(SurfaceHolder holder) 595 { 596 mSurfaceHolder = holder; 597 openVideo(); 598 } 599 600 public void surfaceDestroyed(SurfaceHolder holder) 601 { 602 // after we return from this we can't use the surface any more 603 mSurfaceHolder = null; 604 if (mMediaController != null) mMediaController.hide(); 605 release(true); 606 } 607 }; 608 609 /* 610 * release the media player in any state 611 */ 612 private void release(boolean cleartargetstate) { 613 if (mMediaPlayer != null) { 614 mMediaPlayer.reset(); 615 mMediaPlayer.release(); 616 mMediaPlayer = null; 617 mPendingSubtitleTracks.clear(); 618 mCurrentState = STATE_IDLE; 619 if (cleartargetstate) { 620 mTargetState = STATE_IDLE; 621 } 622 } 623 } 624 625 @Override 626 public boolean onTouchEvent(MotionEvent ev) { 627 if (isInPlaybackState() && mMediaController != null) { 628 toggleMediaControlsVisiblity(); 629 } 630 return false; 631 } 632 633 @Override 634 public boolean onTrackballEvent(MotionEvent ev) { 635 if (isInPlaybackState() && mMediaController != null) { 636 toggleMediaControlsVisiblity(); 637 } 638 return false; 639 } 640 641 @Override 642 public boolean onKeyDown(int keyCode, KeyEvent event) 643 { 644 boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && 645 keyCode != KeyEvent.KEYCODE_VOLUME_UP && 646 keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && 647 keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && 648 keyCode != KeyEvent.KEYCODE_MENU && 649 keyCode != KeyEvent.KEYCODE_CALL && 650 keyCode != KeyEvent.KEYCODE_ENDCALL; 651 if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { 652 if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 653 keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { 654 if (mMediaPlayer.isPlaying()) { 655 pause(); 656 mMediaController.show(); 657 } else { 658 start(); 659 mMediaController.hide(); 660 } 661 return true; 662 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { 663 if (!mMediaPlayer.isPlaying()) { 664 start(); 665 mMediaController.hide(); 666 } 667 return true; 668 } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP 669 || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { 670 if (mMediaPlayer.isPlaying()) { 671 pause(); 672 mMediaController.show(); 673 } 674 return true; 675 } else { 676 toggleMediaControlsVisiblity(); 677 } 678 } 679 680 return super.onKeyDown(keyCode, event); 681 } 682 683 private void toggleMediaControlsVisiblity() { 684 if (mMediaController.isShowing()) { 685 mMediaController.hide(); 686 } else { 687 mMediaController.show(); 688 } 689 } 690 691 @Override 692 public void start() { 693 if (isInPlaybackState()) { 694 mMediaPlayer.start(); 695 mCurrentState = STATE_PLAYING; 696 } 697 mTargetState = STATE_PLAYING; 698 } 699 700 @Override 701 public void pause() { 702 if (isInPlaybackState()) { 703 if (mMediaPlayer.isPlaying()) { 704 mMediaPlayer.pause(); 705 mCurrentState = STATE_PAUSED; 706 } 707 } 708 mTargetState = STATE_PAUSED; 709 } 710 711 public void suspend() { 712 release(false); 713 } 714 715 public void resume() { 716 openVideo(); 717 } 718 719 @Override 720 public int getDuration() { 721 if (isInPlaybackState()) { 722 return mMediaPlayer.getDuration(); 723 } 724 725 return -1; 726 } 727 728 @Override 729 public int getCurrentPosition() { 730 if (isInPlaybackState()) { 731 return mMediaPlayer.getCurrentPosition(); 732 } 733 return 0; 734 } 735 736 @Override 737 public void seekTo(int msec) { 738 if (isInPlaybackState()) { 739 mMediaPlayer.seekTo(msec); 740 mSeekWhenPrepared = 0; 741 } else { 742 mSeekWhenPrepared = msec; 743 } 744 } 745 746 @Override 747 public boolean isPlaying() { 748 return isInPlaybackState() && mMediaPlayer.isPlaying(); 749 } 750 751 @Override 752 public int getBufferPercentage() { 753 if (mMediaPlayer != null) { 754 return mCurrentBufferPercentage; 755 } 756 return 0; 757 } 758 759 private boolean isInPlaybackState() { 760 return (mMediaPlayer != null && 761 mCurrentState != STATE_ERROR && 762 mCurrentState != STATE_IDLE && 763 mCurrentState != STATE_PREPARING); 764 } 765 766 @Override 767 public boolean canPause() { 768 return mCanPause; 769 } 770 771 @Override 772 public boolean canSeekBackward() { 773 return mCanSeekBack; 774 } 775 776 @Override 777 public boolean canSeekForward() { 778 return mCanSeekForward; 779 } 780 781 @Override 782 public int getAudioSessionId() { 783 if (mAudioSession == 0) { 784 MediaPlayer foo = new MediaPlayer(); 785 mAudioSession = foo.getAudioSessionId(); 786 foo.release(); 787 } 788 return mAudioSession; 789 } 790 791 @Override 792 protected void onAttachedToWindow() { 793 super.onAttachedToWindow(); 794 795 if (mSubtitleWidget != null) { 796 mSubtitleWidget.onAttachedToWindow(); 797 } 798 } 799 800 @Override 801 protected void onDetachedFromWindow() { 802 super.onDetachedFromWindow(); 803 804 if (mSubtitleWidget != null) { 805 mSubtitleWidget.onDetachedFromWindow(); 806 } 807 } 808 809 @Override 810 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 811 super.onLayout(changed, left, top, right, bottom); 812 813 if (mSubtitleWidget != null) { 814 measureAndLayoutSubtitleWidget(); 815 } 816 } 817 818 @Override 819 public void draw(Canvas canvas) { 820 super.draw(canvas); 821 822 if (mSubtitleWidget != null) { 823 final int saveCount = canvas.save(); 824 canvas.translate(getPaddingLeft(), getPaddingTop()); 825 mSubtitleWidget.draw(canvas); 826 canvas.restoreToCount(saveCount); 827 } 828 } 829 830 /** 831 * Forces a measurement and layout pass for all overlaid views. 832 * 833 * @see #setSubtitleWidget(RenderingWidget) 834 */ 835 private void measureAndLayoutSubtitleWidget() { 836 final int width = getWidth() - getPaddingLeft() - getPaddingRight(); 837 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); 838 839 mSubtitleWidget.setSize(width, height); 840 } 841 842 /** @hide */ 843 @Override 844 public void setSubtitleWidget(RenderingWidget subtitleWidget) { 845 if (mSubtitleWidget == subtitleWidget) { 846 return; 847 } 848 849 final boolean attachedToWindow = isAttachedToWindow(); 850 if (mSubtitleWidget != null) { 851 if (attachedToWindow) { 852 mSubtitleWidget.onDetachedFromWindow(); 853 } 854 855 mSubtitleWidget.setOnChangedListener(null); 856 } 857 858 mSubtitleWidget = subtitleWidget; 859 860 if (subtitleWidget != null) { 861 if (mSubtitlesChangedListener == null) { 862 mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() { 863 @Override 864 public void onChanged(RenderingWidget renderingWidget) { 865 invalidate(); 866 } 867 }; 868 } 869 870 setWillNotDraw(false); 871 subtitleWidget.setOnChangedListener(mSubtitlesChangedListener); 872 873 if (attachedToWindow) { 874 subtitleWidget.onAttachedToWindow(); 875 requestLayout(); 876 } 877 } else { 878 setWillNotDraw(true); 879 } 880 881 invalidate(); 882 } 883 884 /** @hide */ 885 @Override 886 public Looper getSubtitleLooper() { 887 return Looper.getMainLooper(); 888 } 889} 890