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