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