content: /*
   * Copyright (C) 2006 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.widget;
  
  import android.annotation.NonNull;
  import android.annotation.UnsupportedAppUsage;
  import android.app.AlertDialog;
  import android.content.Context;
  import android.content.DialogInterface;
  import android.content.res.Resources;
  import android.graphics.Canvas;
  import android.media.AudioAttributes;
  import android.media.AudioManager;
  import android.media.Cea708CaptionRenderer;
  import android.media.ClosedCaptionRenderer;
  import android.media.MediaFormat;
  import android.media.MediaPlayer;
  import android.media.MediaPlayer.OnCompletionListener;
  import android.media.MediaPlayer.OnErrorListener;
  import android.media.MediaPlayer.OnInfoListener;
  import android.media.Metadata;
  import android.media.SubtitleController;
  import android.media.SubtitleTrack.RenderingWidget;
  import android.media.TtmlRenderer;
  import android.media.WebVttRenderer;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Looper;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.util.Pair;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.SurfaceHolder;
  import android.view.SurfaceView;
  import android.view.View;
  import android.widget.MediaController.MediaPlayerControl;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.util.Map;
  import java.util.Vector;
  
  /**
   * Displays a video file.  The VideoView class
   * can load images from various sources (such as resources or content
   * providers), takes care of computing its measurement from the video so that
   * it can be used in any layout manager, and provides various display options
   * such as scaling and tinting.<p>
   *
   * <em>Note: VideoView does not retain its full state when going into the
   * background.</em>  In particular, it does not restore the current play state,
   * play position, selected tracks, or any subtitle tracks added via
   * {@link #addSubtitleSource addSubtitleSource()}.  Applications should
   * save and restore these on their own in
   * {@link android.app.Activity#onSaveInstanceState} and
   * {@link android.app.Activity#onRestoreInstanceState}.<p>
   * Also note that the audio session id (from {@link #getAudioSessionId}) may
   * change from its previously returned value when the VideoView is restored.
   * <p>
   * By default, VideoView requests audio focus with {@link AudioManager#AUDIOFOCUS_GAIN}. Use
   * {@link #setAudioFocusRequest(int)} to change this behavior.
   * <p>
   * The default {@link AudioAttributes} used during playback have a usage of
   * {@link AudioAttributes#USAGE_MEDIA} and a content type of
   * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
   * modify them.
   */
  public class VideoView extends SurfaceView
          implements MediaPlayerControl, SubtitleController.Anchor {
      private static final String TAG = "VideoView";
  
      // all possible internal states
      private static final int STATE_ERROR = -1;
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private static final int STATE_IDLE = 0;
      private static final int STATE_PREPARING = 1;
      private static final int STATE_PREPARED = 2;
      private static final int STATE_PLAYING = 3;
      private static final int STATE_PAUSED = 4;
      private static final int STATE_PLAYBACK_COMPLETED = 5;
  
      private final Vector<Pair<InputStream, MediaFormat>> mPendingSubtitleTracks = new Vector<>();
  
      // settable by the client
      @UnsupportedAppUsage
      private Uri mUri;
      @UnsupportedAppUsage
      private Map<String, String> mHeaders;
  
      // mCurrentState is a VideoView object's current state.
      // mTargetState is the state that a method caller intends to reach.
      // For instance, regardless the VideoView object's current state,
      // calling pause() intends to bring the object to a target state
      // of STATE_PAUSED.
      @UnsupportedAppUsage
      private int mCurrentState = STATE_IDLE;
      @UnsupportedAppUsage
      private int mTargetState = STATE_IDLE;
  
      // All the stuff we need for playing and showing a video
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private SurfaceHolder mSurfaceHolder = null;
      @UnsupportedAppUsage
      private MediaPlayer mMediaPlayer = null;
      private int mAudioSession;
      @UnsupportedAppUsage
      private int mVideoWidth;
      @UnsupportedAppUsage
      private int mVideoHeight;
      private int mSurfaceWidth;
      private int mSurfaceHeight;
      @UnsupportedAppUsage
      private MediaController mMediaController;
      private OnCompletionListener mOnCompletionListener;
      private MediaPlayer.OnPreparedListener mOnPreparedListener;
      @UnsupportedAppUsage
      private int mCurrentBufferPercentage;
      private OnErrorListener mOnErrorListener;
      private OnInfoListener mOnInfoListener;
      private int mSeekWhenPrepared;  // recording the seek position while preparing
      private boolean mCanPause;
      private boolean mCanSeekBack;
      private boolean mCanSeekForward;
      private AudioManager mAudioManager;
      private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
      private AudioAttributes mAudioAttributes;
  
      /** Subtitle rendering widget overlaid on top of the video. */
      private RenderingWidget mSubtitleWidget;
  
      /** Listener for changes to subtitle data, used to redraw when needed. */
      private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
  
      public VideoView(Context context) {
          this(context, null);
      }
  
      public VideoView(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          mVideoWidth = 0;
          mVideoHeight = 0;
  
          mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
          mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
                  .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
  
          getHolder().addCallback(mSHCallback);
          getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  
          setFocusable(true);
          setFocusableInTouchMode(true);
          requestFocus();
  
          mCurrentState = STATE_IDLE;
          mTargetState = STATE_IDLE;
      }
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
          //        + MeasureSpec.toString(heightMeasureSpec) + ")");
  
          int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
          int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
          if (mVideoWidth > 0 && mVideoHeight > 0) {
  
              int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
              int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
              int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
              int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  
              if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
                  // the size is fixed
                  width = widthSpecSize;
                  height = heightSpecSize;
  
                  // for compatibility, we adjust size based on aspect ratio
                  if ( mVideoWidth * height  < width * mVideoHeight ) {
                      //Log.i("@@@", "image too wide, correcting");
                      width = height * mVideoWidth / mVideoHeight;
                  } else if ( mVideoWidth * height  > width * mVideoHeight ) {
                      //Log.i("@@@", "image too tall, correcting");
                      height = width * mVideoHeight / mVideoWidth;
                  }
              } else if (widthSpecMode == MeasureSpec.EXACTLY) {
                  // only the width is fixed, adjust the height to match aspect ratio if possible
                  width = widthSpecSize;
                  height = width * mVideoHeight / mVideoWidth;
                  if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
                      // couldn't match aspect ratio within the constraints
                      height = heightSpecSize;
                  }
              } else if (heightSpecMode == MeasureSpec.EXACTLY) {
                  // only the height is fixed, adjust the width to match aspect ratio if possible
                  height = heightSpecSize;
                  width = height * mVideoWidth / mVideoHeight;
                  if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
                      // couldn't match aspect ratio within the constraints
                      width = widthSpecSize;
                  }
              } else {
                  // neither the width nor the height are fixed, try to use actual video size
                  width = mVideoWidth;
                  height = mVideoHeight;
                  if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
                      // too tall, decrease both width and height
                      height = heightSpecSize;
                      width = height * mVideoWidth / mVideoHeight;
                  }
                  if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
                      // too wide, decrease both width and height
                      width = widthSpecSize;
                      height = width * mVideoHeight / mVideoWidth;
                  }
              }
          } else {
              // no size yet, just adopt the given spec sizes
          }
          setMeasuredDimension(width, height);
      }
  
      @Override
      public CharSequence getAccessibilityClassName() {
          return VideoView.class.getName();
      }
  
      public int resolveAdjustedSize(int desiredSize, int measureSpec) {
          return getDefaultSize(desiredSize, measureSpec);
      }
  
      /**
       * Sets video path.
       *
       * @param path the path of the video.
       */
      public void setVideoPath(String path) {
          setVideoURI(Uri.parse(path));
      }
  
      /**
       * Sets video URI.
       *
       * @param uri the URI of the video.
       */
      public void setVideoURI(Uri uri) {
          setVideoURI(uri, null);
      }
  
      /**
       * Sets video URI using specific headers.
       *
       * @param uri     the URI of the video.
       * @param headers the headers for the URI request.
       *                Note that the cross domain redirection is allowed by default, but that can be
       *                changed with key/value pairs through the headers parameter with
       *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
       *                to disallow or allow cross domain redirection.
       */
      public void setVideoURI(Uri uri, Map<String, String> headers) {
          mUri = uri;
          mHeaders = headers;
          mSeekWhenPrepared = 0;
          openVideo();
          requestLayout();
          invalidate();
      }
  
      /**
       * Sets which type of audio focus will be requested during the playback, or configures playback
       * to not request audio focus. Valid values for focus requests are
       * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
       * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
       * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
       * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
       * requested when playback starts. You can for instance use this when playing a silent animation
       * through this class, and you don't want to affect other audio applications playing in the
       * background.
       * @param focusGain the type of audio focus gain that will be requested, or
       *    {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during playback.
       */
      public void setAudioFocusRequest(int focusGain) {
          if (focusGain != AudioManager.AUDIOFOCUS_NONE
                  && focusGain != AudioManager.AUDIOFOCUS_GAIN
                  && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
                  && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
                  && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
              throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
          }
          mAudioFocusType = focusGain;
      }
  
      /**
       * Sets the {@link AudioAttributes} to be used during the playback of the video.
       * @param attributes non-null <code>AudioAttributes</code>.
       */
      public void setAudioAttributes(@NonNull AudioAttributes attributes) {
          if (attributes == null) {
              throw new IllegalArgumentException("Illegal null AudioAttributes");
          }
          mAudioAttributes = attributes;
      }
  
      /**
       * Adds an external subtitle source file (from the provided input stream.)
       *
       * Note that a single external subtitle source may contain multiple or no
       * supported tracks in it. If the source contained at least one track in
       * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
       * info message. Otherwise, if reading the source takes excessive time,
       * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
       * message. If the source contained no supported track (including an empty
       * source file or null input stream), one will receive a {@link
       * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the
       * total number of available tracks using {@link MediaPlayer#getTrackInfo()}
       * to see what additional tracks become available after this method call.
       *
       * @param is     input stream containing the subtitle data.  It will be
       *               closed by the media framework.
       * @param format the format of the subtitle track(s).  Must contain at least
       *               the mime type ({@link MediaFormat#KEY_MIME}) and the
       *               language ({@link MediaFormat#KEY_LANGUAGE}) of the file.
       *               If the file itself contains the language information,
       *               specify "und" for the language.
       */
      public void addSubtitleSource(InputStream is, MediaFormat format) {
          if (mMediaPlayer == null) {
              mPendingSubtitleTracks.add(Pair.create(is, format));
          } else {
              try {
                  mMediaPlayer.addSubtitleSource(is, format);
              } catch (IllegalStateException e) {
                  mInfoListener.onInfo(
                          mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
              }
          }
      }
  
      public void stopPlayback() {
          if (mMediaPlayer != null) {
              mMediaPlayer.stop();
              mMediaPlayer.release();
              mMediaPlayer = null;
              mCurrentState = STATE_IDLE;
              mTargetState  = STATE_IDLE;
              mAudioManager.abandonAudioFocus(null);
          }
      }
  
      private void openVideo() {
          if (mUri == null || mSurfaceHolder == null) {
              // not ready for playback just yet, will try again later
              return;
          }
          // we shouldn't clear the target state, because somebody might have
          // called start() previously
          release(false);
  
          if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
              // TODO this should have a focus listener
              mAudioManager.requestAudioFocus(null, mAudioAttributes, mAudioFocusType, 0 /*flags*/);
          }
  
          try {
              mMediaPlayer = new MediaPlayer();
              // TODO: create SubtitleController in MediaPlayer, but we need
              // a context for the subtitle renderers
              final Context context = getContext();
              final SubtitleController controller = new SubtitleController(
                      context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
              controller.registerRenderer(new WebVttRenderer(context));
              controller.registerRenderer(new TtmlRenderer(context));
              controller.registerRenderer(new Cea708CaptionRenderer(context));
              controller.registerRenderer(new ClosedCaptionRenderer(context));
              mMediaPlayer.setSubtitleAnchor(controller, this);
  
              if (mAudioSession != 0) {
                  mMediaPlayer.setAudioSessionId(mAudioSession);
              } else {
                  mAudioSession = mMediaPlayer.getAudioSessionId();
              }
              mMediaPlayer.setOnPreparedListener(mPreparedListener);
              mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
              mMediaPlayer.setOnCompletionListener(mCompletionListener);
              mMediaPlayer.setOnErrorListener(mErrorListener);
              mMediaPlayer.setOnInfoListener(mInfoListener);
              mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
              mCurrentBufferPercentage = 0;
              mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
              mMediaPlayer.setDisplay(mSurfaceHolder);
              mMediaPlayer.setAudioAttributes(mAudioAttributes);
              mMediaPlayer.setScreenOnWhilePlaying(true);
              mMediaPlayer.prepareAsync();
  
              for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) {
                  try {
                      mMediaPlayer.addSubtitleSource(pending.first, pending.second);
                  } catch (IllegalStateException e) {
                      mInfoListener.onInfo(
                              mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
                  }
              }
  
              // we don't set the target state here either, but preserve the
              // target state that was there before.
              mCurrentState = STATE_PREPARING;
              attachMediaController();
          } catch (IOException ex) {
              Log.w(TAG, "Unable to open content: " + mUri, ex);
              mCurrentState = STATE_ERROR;
              mTargetState = STATE_ERROR;
              mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
              return;
          } catch (IllegalArgumentException ex) {
              Log.w(TAG, "Unable to open content: " + mUri, ex);
              mCurrentState = STATE_ERROR;
              mTargetState = STATE_ERROR;
              mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
              return;
          } finally {
              mPendingSubtitleTracks.clear();
          }
      }
  
      public void setMediaController(MediaController controller) {
          if (mMediaController != null) {
              mMediaController.hide();
          }
          mMediaController = controller;
          attachMediaController();
      }
  
      private void attachMediaController() {
          if (mMediaPlayer != null && mMediaController != null) {
              mMediaController.setMediaPlayer(this);
              View anchorView = this.getParent() instanceof View ?
                      (View)this.getParent() : this;
              mMediaController.setAnchorView(anchorView);
              mMediaController.setEnabled(isInPlaybackState());
          }
      }
  
      MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
          new MediaPlayer.OnVideoSizeChangedListener() {
              public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                  mVideoWidth = mp.getVideoWidth();
                  mVideoHeight = mp.getVideoHeight();
                  if (mVideoWidth != 0 && mVideoHeight != 0) {
                      getHolder().setFixedSize(mVideoWidth, mVideoHeight);
                      requestLayout();
                  }
              }
      };
  
      @UnsupportedAppUsage
      MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
          public void onPrepared(MediaPlayer mp) {
              mCurrentState = STATE_PREPARED;
  
              // Get the capabilities of the player for this stream
              Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
                                        MediaPlayer.BYPASS_METADATA_FILTER);
  
              if (data != null) {
                  mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
                          || data.getBoolean(Metadata.PAUSE_AVAILABLE);
                  mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
                          || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
                  mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
                          || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
              } else {
                  mCanPause = mCanSeekBack = mCanSeekForward = true;
              }
  
              if (mOnPreparedListener != null) {
                  mOnPreparedListener.onPrepared(mMediaPlayer);
              }
              if (mMediaController != null) {
                  mMediaController.setEnabled(true);
              }
              mVideoWidth = mp.getVideoWidth();
              mVideoHeight = mp.getVideoHeight();
  
              int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
              if (seekToPosition != 0) {
                  seekTo(seekToPosition);
              }
              if (mVideoWidth != 0 && mVideoHeight != 0) {
                  //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
                  getHolder().setFixedSize(mVideoWidth, mVideoHeight);
                  if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
                      // We didn't actually change the size (it was already at the size
                      // we need), so we won't get a "surface changed" callback, so
                      // start the video here instead of in the callback.
                      if (mTargetState == STATE_PLAYING) {
                          start();
                          if (mMediaController != null) {
                              mMediaController.show();
                          }
                      } else if (!isPlaying() &&
                                 (seekToPosition != 0 || getCurrentPosition() > 0)) {
                         if (mMediaController != null) {
                             // Show the media controls when we're paused into a video and make 'em stick.
                             mMediaController.show(0);
                         }
                     }
                  }
              } else {
                  // We don't know the video size yet, but should start anyway.
                  // The video size might be reported to us later.
                  if (mTargetState == STATE_PLAYING) {
                      start();
                  }
              }
          }
      };
  
      private MediaPlayer.OnCompletionListener mCompletionListener =
          new MediaPlayer.OnCompletionListener() {
          public void onCompletion(MediaPlayer mp) {
              mCurrentState = STATE_PLAYBACK_COMPLETED;
              mTargetState = STATE_PLAYBACK_COMPLETED;
              if (mMediaController != null) {
                  mMediaController.hide();
              }
              if (mOnCompletionListener != null) {
                  mOnCompletionListener.onCompletion(mMediaPlayer);
              }
              if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
                  mAudioManager.abandonAudioFocus(null);
              }
          }
      };
  
      private MediaPlayer.OnInfoListener mInfoListener =
          new MediaPlayer.OnInfoListener() {
          public  boolean onInfo(MediaPlayer mp, int arg1, int arg2) {
              if (mOnInfoListener != null) {
                  mOnInfoListener.onInfo(mp, arg1, arg2);
              }
              return true;
          }
      };
  
      @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
      private MediaPlayer.OnErrorListener mErrorListener =
          new MediaPlayer.OnErrorListener() {
          public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
              Log.d(TAG, "Error: " + framework_err + "," + impl_err);
              mCurrentState = STATE_ERROR;
              mTargetState = STATE_ERROR;
              if (mMediaController != null) {
                  mMediaController.hide();
              }
  
              /* If an error handler has been supplied, use it and finish. */
              if (mOnErrorListener != null) {
                  if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
                      return true;
                  }
              }
  
              /* Otherwise, pop up an error dialog so the user knows that
               * something bad has happened. Only try and pop up the dialog
               * if we're attached to a window. When we're going away and no
               * longer have a window, don't bother showing the user an error.
               */
              if (getWindowToken() != null) {
                  Resources r = mContext.getResources();
                  int messageId;
  
                  if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
                      messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
                  } else {
                      messageId = com.android.internal.R.string.VideoView_error_text_unknown;
                  }
  
                  new AlertDialog.Builder(mContext)
                          .setMessage(messageId)
                          .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
                                  new DialogInterface.OnClickListener() {
                                      public void onClick(DialogInterface dialog, int whichButton) {
                                          /* If we get here, there is no onError listener, so
                                           * at least inform them that the video is over.
                                           */
                                          if (mOnCompletionListener != null) {
                                              mOnCompletionListener.onCompletion(mMediaPlayer);
                                          }
                                      }
                                  })
                          .setCancelable(false)
                          .show();
              }
              return true;
          }
      };
  
      private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
          new MediaPlayer.OnBufferingUpdateListener() {
          public void onBufferingUpdate(MediaPlayer mp, int percent) {
              mCurrentBufferPercentage = percent;
          }
      };
  
      /**
       * Register a callback to be invoked when the media file
       * is loaded and ready to go.
       *
       * @param l The callback that will be run
       */
      public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
      {
          mOnPreparedListener = l;
      }
  
      /**
       * Register a callback to be invoked when the end of a media file
       * has been reached during playback.
       *
       * @param l The callback that will be run
       */
      public void setOnCompletionListener(OnCompletionListener l)
      {
          mOnCompletionListener = l;
      }
  
      /**
       * Register a callback to be invoked when an error occurs
       * during playback or setup.  If no listener is specified,
       * or if the listener returned false, VideoView will inform
       * the user of any errors.
       *
       * @param l The callback that will be run
       */
      public void setOnErrorListener(OnErrorListener l)
      {
          mOnErrorListener = l;
      }
  
      /**
       * Register a callback to be invoked when an informational event
       * occurs during playback or setup.
       *
       * @param l The callback that will be run
       */
      public void setOnInfoListener(OnInfoListener l) {
          mOnInfoListener = l;
      }
  
      @UnsupportedAppUsage
      SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
      {
          public void surfaceChanged(SurfaceHolder holder, int format,
                                      int w, int h)
          {
              mSurfaceWidth = w;
              mSurfaceHeight = h;
              boolean isValidState =  (mTargetState == STATE_PLAYING);
              boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
              if (mMediaPlayer != null && isValidState && hasValidSize) {
                  if (mSeekWhenPrepared != 0) {
                      seekTo(mSeekWhenPrepared);
                  }
                  start();
              }
          }
  
          public void surfaceCreated(SurfaceHolder holder)
          {
              mSurfaceHolder = holder;
              openVideo();
          }
  
          public void surfaceDestroyed(SurfaceHolder holder)
          {
              // after we return from this we can't use the surface any more
              mSurfaceHolder = null;
              if (mMediaController != null) mMediaController.hide();
              release(true);
          }
      };
  
      /*
       * release the media player in any state
       */
      @UnsupportedAppUsage
      private void release(boolean cleartargetstate) {
          if (mMediaPlayer != null) {
              mMediaPlayer.reset();
              mMediaPlayer.release();
              mMediaPlayer = null;
              mPendingSubtitleTracks.clear();
              mCurrentState = STATE_IDLE;
              if (cleartargetstate) {
                  mTargetState  = STATE_IDLE;
              }
              if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
                  mAudioManager.abandonAudioFocus(null);
              }
          }
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN
                  && isInPlaybackState() && mMediaController != null) {
              toggleMediaControlsVisiblity();
          }
          return super.onTouchEvent(ev);
      }
  
      @Override
      public boolean onTrackballEvent(MotionEvent ev) {
          if (ev.getAction() == MotionEvent.ACTION_DOWN
                  && isInPlaybackState() && mMediaController != null) {
              toggleMediaControlsVisiblity();
          }
          return super.onTrackballEvent(ev);
      }
  
      @Override
      public boolean onKeyDown(int keyCode, KeyEvent event)
      {
          boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
                                       keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
                                       keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
                                       keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
                                       keyCode != KeyEvent.KEYCODE_MENU &&
                                       keyCode != KeyEvent.KEYCODE_CALL &&
                                       keyCode != KeyEvent.KEYCODE_ENDCALL;
          if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
              if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
                      keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
                  if (mMediaPlayer.isPlaying()) {
                      pause();
                      mMediaController.show();
                  } else {
                      start();
                      mMediaController.hide();
                  }
                  return true;
              } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
                  if (!mMediaPlayer.isPlaying()) {
                      start();
                      mMediaController.hide();
                  }
                  return true;
              } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
                      || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
                  if (mMediaPlayer.isPlaying()) {
                      pause();
                      mMediaController.show();
                  }
                  return true;
              } else {
                  toggleMediaControlsVisiblity();
              }
          }
  
          return super.onKeyDown(keyCode, event);
      }
  
      private void toggleMediaControlsVisiblity() {
          if (mMediaController.isShowing()) {
              mMediaController.hide();
          } else {
              mMediaController.show();
          }
      }
  
      @Override
      public void start() {
          if (isInPlaybackState()) {
              mMediaPlayer.start();
              mCurrentState = STATE_PLAYING;
          }
          mTargetState = STATE_PLAYING;
      }
  
      @Override
      public void pause() {
          if (isInPlaybackState()) {
              if (mMediaPlayer.isPlaying()) {
                  mMediaPlayer.pause();
                  mCurrentState = STATE_PAUSED;
              }
          }
          mTargetState = STATE_PAUSED;
      }
  
      public void suspend() {
          release(false);
      }
  
      public void resume() {
          openVideo();
      }
  
      @Override
      public int getDuration() {
          if (isInPlaybackState()) {
              return mMediaPlayer.getDuration();
          }
  
          return -1;
      }
  
      @Override
      public int getCurrentPosition() {
          if (isInPlaybackState()) {
              return mMediaPlayer.getCurrentPosition();
          }
          return 0;
      }
  
      @Override
      public void seekTo(int msec) {
          if (isInPlaybackState()) {
              mMediaPlayer.seekTo(msec);
              mSeekWhenPrepared = 0;
          } else {
              mSeekWhenPrepared = msec;
          }
      }
  
      @Override
      public boolean isPlaying() {
          return isInPlaybackState() && mMediaPlayer.isPlaying();
      }
  
      @Override
      public int getBufferPercentage() {
          if (mMediaPlayer != null) {
              return mCurrentBufferPercentage;
          }
          return 0;
      }
  
      private boolean isInPlaybackState() {
          return (mMediaPlayer != null &&
                  mCurrentState != STATE_ERROR &&
                  mCurrentState != STATE_IDLE &&
                  mCurrentState != STATE_PREPARING);
      }
  
      @Override
      public boolean canPause() {
          return mCanPause;
      }
  
      @Override
      public boolean canSeekBackward() {
          return mCanSeekBack;
      }
  
      @Override
      public boolean canSeekForward() {
          return mCanSeekForward;
      }
  
      @Override
      public int getAudioSessionId() {
          if (mAudioSession == 0) {
              MediaPlayer foo = new MediaPlayer();
              mAudioSession = foo.getAudioSessionId();
              foo.release();
          }
          return mAudioSession;
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
  
          if (mSubtitleWidget != null) {
              mSubtitleWidget.onAttachedToWindow();
          }
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
  
          if (mSubtitleWidget != null) {
              mSubtitleWidget.onDetachedFromWindow();
          }
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          super.onLayout(changed, left, top, right, bottom);
  
          if (mSubtitleWidget != null) {
              measureAndLayoutSubtitleWidget();
          }
      }
  
      @Override
      public void draw(Canvas canvas) {
          super.draw(canvas);
  
          if (mSubtitleWidget != null) {
              final int saveCount = canvas.save();
              canvas.translate(getPaddingLeft(), getPaddingTop());
              mSubtitleWidget.draw(canvas);
              canvas.restoreToCount(saveCount);
          }
      }
  
      /**
       * Forces a measurement and layout pass for all overlaid views.
       *
       * @see #setSubtitleWidget(RenderingWidget)
       */
      private void measureAndLayoutSubtitleWidget() {
          final int width = getWidth() - getPaddingLeft() - getPaddingRight();
          final int height = getHeight() - getPaddingTop() - getPaddingBottom();
  
          mSubtitleWidget.setSize(width, height);
      }
  
      /** @hide */
      @Override
      public void setSubtitleWidget(RenderingWidget subtitleWidget) {
          if (mSubtitleWidget == subtitleWidget) {
              return;
          }
  
          final boolean attachedToWindow = isAttachedToWindow();
          if (mSubtitleWidget != null) {
              if (attachedToWindow) {
                  mSubtitleWidget.onDetachedFromWindow();
              }
  
              mSubtitleWidget.setOnChangedListener(null);
          }
  
          mSubtitleWidget = subtitleWidget;
  
          if (subtitleWidget != null) {
              if (mSubtitlesChangedListener == null) {
                  mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
                      @Override
                      public void onChanged(RenderingWidget renderingWidget) {
                          invalidate();
                      }
                  };
              }
  
              setWillNotDraw(false);
              subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
  
              if (attachedToWindow) {
                  subtitleWidget.onAttachedToWindow();
                  requestLayout();
              }
          } else {
              setWillNotDraw(true);
          }
  
          invalidate();
      }
  
      /** @hide */
      @Override
      public Looper getSubtitleLooper() {
          return Looper.getMainLooper();
      }
  }
  