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