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