• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.annotation.NonNull;
20 import android.app.AlertDialog;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.res.Resources;
24 import android.graphics.Canvas;
25 import android.media.AudioAttributes;
26 import android.media.AudioManager;
27 import android.media.Cea708CaptionRenderer;
28 import android.media.ClosedCaptionRenderer;
29 import android.media.MediaFormat;
30 import android.media.MediaPlayer;
31 import android.media.MediaPlayer.OnCompletionListener;
32 import android.media.MediaPlayer.OnErrorListener;
33 import android.media.MediaPlayer.OnInfoListener;
34 import android.media.Metadata;
35 import android.media.SubtitleController;
36 import android.media.SubtitleTrack.RenderingWidget;
37 import android.media.TtmlRenderer;
38 import android.media.WebVttRenderer;
39 import android.net.Uri;
40 import android.os.Looper;
41 import android.util.AttributeSet;
42 import android.util.Log;
43 import android.util.Pair;
44 import android.view.KeyEvent;
45 import android.view.MotionEvent;
46 import android.view.SurfaceHolder;
47 import android.view.SurfaceView;
48 import android.view.View;
49 import android.widget.MediaController.MediaPlayerControl;
50 
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.util.Map;
54 import 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  */
81 public 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 
VideoView(Context context)136     public VideoView(Context context) {
137         this(context, null);
138     }
139 
VideoView(Context context, AttributeSet attrs)140     public VideoView(Context context, AttributeSet attrs) {
141         this(context, attrs, 0);
142     }
143 
VideoView(Context context, AttributeSet attrs, int defStyleAttr)144     public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
145         this(context, attrs, defStyleAttr, 0);
146     }
147 
VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
getAccessibilityClassName()234     public CharSequence getAccessibilityClassName() {
235         return VideoView.class.getName();
236     }
237 
resolveAdjustedSize(int desiredSize, int measureSpec)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      */
setVideoPath(String path)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      */
setVideoURI(Uri uri)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      */
setVideoURI(Uri uri, Map<String, String> headers)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      */
setAudioFocusRequest(int focusGain)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      */
setAudioAttributes(@onNull AudioAttributes attributes)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      */
addSubtitleSource(InputStream is, MediaFormat format)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 
stopPlayback()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 
openVideo()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 
setMediaController(MediaController controller)435     public void setMediaController(MediaController controller) {
436         if (mMediaController != null) {
437             mMediaController.hide();
438         }
439         mMediaController = controller;
440         attachMediaController();
441     }
442 
attachMediaController()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      */
setOnPreparedListener(MediaPlayer.OnPreparedListener l)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      */
setOnCompletionListener(OnCompletionListener l)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      */
setOnErrorListener(OnErrorListener l)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      */
setOnInfoListener(OnInfoListener l)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      */
release(boolean cleartargetstate)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
onTouchEvent(MotionEvent ev)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
onTrackballEvent(MotionEvent ev)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
onKeyDown(int keyCode, KeyEvent event)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 
toggleMediaControlsVisiblity()769     private void toggleMediaControlsVisiblity() {
770         if (mMediaController.isShowing()) {
771             mMediaController.hide();
772         } else {
773             mMediaController.show();
774         }
775     }
776 
777     @Override
start()778     public void start() {
779         if (isInPlaybackState()) {
780             mMediaPlayer.start();
781             mCurrentState = STATE_PLAYING;
782         }
783         mTargetState = STATE_PLAYING;
784     }
785 
786     @Override
pause()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 
suspend()797     public void suspend() {
798         release(false);
799     }
800 
resume()801     public void resume() {
802         openVideo();
803     }
804 
805     @Override
getDuration()806     public int getDuration() {
807         if (isInPlaybackState()) {
808             return mMediaPlayer.getDuration();
809         }
810 
811         return -1;
812     }
813 
814     @Override
getCurrentPosition()815     public int getCurrentPosition() {
816         if (isInPlaybackState()) {
817             return mMediaPlayer.getCurrentPosition();
818         }
819         return 0;
820     }
821 
822     @Override
seekTo(int msec)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
isPlaying()833     public boolean isPlaying() {
834         return isInPlaybackState() && mMediaPlayer.isPlaying();
835     }
836 
837     @Override
getBufferPercentage()838     public int getBufferPercentage() {
839         if (mMediaPlayer != null) {
840             return mCurrentBufferPercentage;
841         }
842         return 0;
843     }
844 
isInPlaybackState()845     private boolean isInPlaybackState() {
846         return (mMediaPlayer != null &&
847                 mCurrentState != STATE_ERROR &&
848                 mCurrentState != STATE_IDLE &&
849                 mCurrentState != STATE_PREPARING);
850     }
851 
852     @Override
canPause()853     public boolean canPause() {
854         return mCanPause;
855     }
856 
857     @Override
canSeekBackward()858     public boolean canSeekBackward() {
859         return mCanSeekBack;
860     }
861 
862     @Override
canSeekForward()863     public boolean canSeekForward() {
864         return mCanSeekForward;
865     }
866 
867     @Override
getAudioSessionId()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
onAttachedToWindow()878     protected void onAttachedToWindow() {
879         super.onAttachedToWindow();
880 
881         if (mSubtitleWidget != null) {
882             mSubtitleWidget.onAttachedToWindow();
883         }
884     }
885 
886     @Override
onDetachedFromWindow()887     protected void onDetachedFromWindow() {
888         super.onDetachedFromWindow();
889 
890         if (mSubtitleWidget != null) {
891             mSubtitleWidget.onDetachedFromWindow();
892         }
893     }
894 
895     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)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
draw(Canvas canvas)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      */
measureAndLayoutSubtitleWidget()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
setSubtitleWidget(RenderingWidget subtitleWidget)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
getSubtitleLooper()972     public Looper getSubtitleLooper() {
973         return Looper.getMainLooper();
974     }
975 }
976