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