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