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