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