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