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
17package android.widget;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.media.AudioManager;
25import android.media.MediaPlayer;
26import android.media.Metadata;
27import android.media.MediaPlayer.OnCompletionListener;
28import android.media.MediaPlayer.OnErrorListener;
29import android.media.MediaPlayer.OnInfoListener;
30import android.net.Uri;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.KeyEvent;
34import android.view.MotionEvent;
35import android.view.SurfaceHolder;
36import android.view.SurfaceView;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityNodeInfo;
40import android.widget.MediaController.MediaPlayerControl;
41
42import java.io.IOException;
43import java.util.Map;
44
45/**
46 * Displays a video file.  The VideoView class
47 * can load images from various sources (such as resources or content
48 * providers), takes care of computing its measurement from the video so that
49 * it can be used in any layout manager, and provides various display options
50 * such as scaling and tinting.
51 */
52public class VideoView extends SurfaceView implements MediaPlayerControl {
53    private String TAG = "VideoView";
54    // settable by the client
55    private Uri         mUri;
56    private Map<String, String> mHeaders;
57    private int         mDuration;
58
59    // all possible internal states
60    private static final int STATE_ERROR              = -1;
61    private static final int STATE_IDLE               = 0;
62    private static final int STATE_PREPARING          = 1;
63    private static final int STATE_PREPARED           = 2;
64    private static final int STATE_PLAYING            = 3;
65    private static final int STATE_PAUSED             = 4;
66    private static final int STATE_PLAYBACK_COMPLETED = 5;
67
68    // mCurrentState is a VideoView object's current state.
69    // mTargetState is the state that a method caller intends to reach.
70    // For instance, regardless the VideoView object's current state,
71    // calling pause() intends to bring the object to a target state
72    // of STATE_PAUSED.
73    private int mCurrentState = STATE_IDLE;
74    private int mTargetState  = STATE_IDLE;
75
76    // All the stuff we need for playing and showing a video
77    private SurfaceHolder mSurfaceHolder = null;
78    private MediaPlayer mMediaPlayer = null;
79    private int         mVideoWidth;
80    private int         mVideoHeight;
81    private int         mSurfaceWidth;
82    private int         mSurfaceHeight;
83    private MediaController mMediaController;
84    private OnCompletionListener mOnCompletionListener;
85    private MediaPlayer.OnPreparedListener mOnPreparedListener;
86    private int         mCurrentBufferPercentage;
87    private OnErrorListener mOnErrorListener;
88    private OnInfoListener  mOnInfoListener;
89    private int         mSeekWhenPrepared;  // recording the seek position while preparing
90    private boolean     mCanPause;
91    private boolean     mCanSeekBack;
92    private boolean     mCanSeekForward;
93
94    public VideoView(Context context) {
95        super(context);
96        initVideoView();
97    }
98
99    public VideoView(Context context, AttributeSet attrs) {
100        this(context, attrs, 0);
101        initVideoView();
102    }
103
104    public VideoView(Context context, AttributeSet attrs, int defStyle) {
105        super(context, attrs, defStyle);
106        initVideoView();
107    }
108
109    @Override
110    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
111        //Log.i("@@@@", "onMeasure");
112        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
113        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
114        if (mVideoWidth > 0 && mVideoHeight > 0) {
115            if ( mVideoWidth * height  > width * mVideoHeight ) {
116                //Log.i("@@@", "image too tall, correcting");
117                height = width * mVideoHeight / mVideoWidth;
118            } else if ( mVideoWidth * height  < width * mVideoHeight ) {
119                //Log.i("@@@", "image too wide, correcting");
120                width = height * mVideoWidth / mVideoHeight;
121            } else {
122                //Log.i("@@@", "aspect ratio is correct: " +
123                        //width+"/"+height+"="+
124                        //mVideoWidth+"/"+mVideoHeight);
125            }
126        }
127        //Log.i("@@@@@@@@@@", "setting size: " + width + 'x' + height);
128        setMeasuredDimension(width, height);
129    }
130
131    @Override
132    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
133        super.onInitializeAccessibilityEvent(event);
134        event.setClassName(VideoView.class.getName());
135    }
136
137    @Override
138    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
139        super.onInitializeAccessibilityNodeInfo(info);
140        info.setClassName(VideoView.class.getName());
141    }
142
143    public int resolveAdjustedSize(int desiredSize, int measureSpec) {
144        int result = desiredSize;
145        int specMode = MeasureSpec.getMode(measureSpec);
146        int specSize =  MeasureSpec.getSize(measureSpec);
147
148        switch (specMode) {
149            case MeasureSpec.UNSPECIFIED:
150                /* Parent says we can be as big as we want. Just don't be larger
151                 * than max size imposed on ourselves.
152                 */
153                result = desiredSize;
154                break;
155
156            case MeasureSpec.AT_MOST:
157                /* Parent says we can be as big as we want, up to specSize.
158                 * Don't be larger than specSize, and don't be larger than
159                 * the max size imposed on ourselves.
160                 */
161                result = Math.min(desiredSize, specSize);
162                break;
163
164            case MeasureSpec.EXACTLY:
165                // No choice. Do what we are told.
166                result = specSize;
167                break;
168        }
169        return result;
170}
171
172    private void initVideoView() {
173        mVideoWidth = 0;
174        mVideoHeight = 0;
175        getHolder().addCallback(mSHCallback);
176        getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
177        setFocusable(true);
178        setFocusableInTouchMode(true);
179        requestFocus();
180        mCurrentState = STATE_IDLE;
181        mTargetState  = STATE_IDLE;
182    }
183
184    public void setVideoPath(String path) {
185        setVideoURI(Uri.parse(path));
186    }
187
188    public void setVideoURI(Uri uri) {
189        setVideoURI(uri, null);
190    }
191
192    /**
193     * @hide
194     */
195    public void setVideoURI(Uri uri, Map<String, String> headers) {
196        mUri = uri;
197        mHeaders = headers;
198        mSeekWhenPrepared = 0;
199        openVideo();
200        requestLayout();
201        invalidate();
202    }
203
204    public void stopPlayback() {
205        if (mMediaPlayer != null) {
206            mMediaPlayer.stop();
207            mMediaPlayer.release();
208            mMediaPlayer = null;
209            mCurrentState = STATE_IDLE;
210            mTargetState  = STATE_IDLE;
211        }
212    }
213
214    private void openVideo() {
215        if (mUri == null || mSurfaceHolder == null) {
216            // not ready for playback just yet, will try again later
217            return;
218        }
219        // Tell the music playback service to pause
220        // TODO: these constants need to be published somewhere in the framework.
221        Intent i = new Intent("com.android.music.musicservicecommand");
222        i.putExtra("command", "pause");
223        mContext.sendBroadcast(i);
224
225        // we shouldn't clear the target state, because somebody might have
226        // called start() previously
227        release(false);
228        try {
229            mMediaPlayer = new MediaPlayer();
230            mMediaPlayer.setOnPreparedListener(mPreparedListener);
231            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
232            mDuration = -1;
233            mMediaPlayer.setOnCompletionListener(mCompletionListener);
234            mMediaPlayer.setOnErrorListener(mErrorListener);
235            mMediaPlayer.setOnInfoListener(mOnInfoListener);
236            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
237            mCurrentBufferPercentage = 0;
238            mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
239            mMediaPlayer.setDisplay(mSurfaceHolder);
240            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
241            mMediaPlayer.setScreenOnWhilePlaying(true);
242            mMediaPlayer.prepareAsync();
243            // we don't set the target state here either, but preserve the
244            // target state that was there before.
245            mCurrentState = STATE_PREPARING;
246            attachMediaController();
247        } catch (IOException ex) {
248            Log.w(TAG, "Unable to open content: " + mUri, ex);
249            mCurrentState = STATE_ERROR;
250            mTargetState = STATE_ERROR;
251            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
252            return;
253        } catch (IllegalArgumentException ex) {
254            Log.w(TAG, "Unable to open content: " + mUri, ex);
255            mCurrentState = STATE_ERROR;
256            mTargetState = STATE_ERROR;
257            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
258            return;
259        }
260    }
261
262    public void setMediaController(MediaController controller) {
263        if (mMediaController != null) {
264            mMediaController.hide();
265        }
266        mMediaController = controller;
267        attachMediaController();
268    }
269
270    private void attachMediaController() {
271        if (mMediaPlayer != null && mMediaController != null) {
272            mMediaController.setMediaPlayer(this);
273            View anchorView = this.getParent() instanceof View ?
274                    (View)this.getParent() : this;
275            mMediaController.setAnchorView(anchorView);
276            mMediaController.setEnabled(isInPlaybackState());
277        }
278    }
279
280    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
281        new MediaPlayer.OnVideoSizeChangedListener() {
282            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
283                mVideoWidth = mp.getVideoWidth();
284                mVideoHeight = mp.getVideoHeight();
285                if (mVideoWidth != 0 && mVideoHeight != 0) {
286                    getHolder().setFixedSize(mVideoWidth, mVideoHeight);
287                    requestLayout();
288                }
289            }
290    };
291
292    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
293        public void onPrepared(MediaPlayer mp) {
294            mCurrentState = STATE_PREPARED;
295
296            // Get the capabilities of the player for this stream
297            Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
298                                      MediaPlayer.BYPASS_METADATA_FILTER);
299
300            if (data != null) {
301                mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
302                        || data.getBoolean(Metadata.PAUSE_AVAILABLE);
303                mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
304                        || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
305                mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
306                        || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
307            } else {
308                mCanPause = mCanSeekBack = mCanSeekForward = true;
309            }
310
311            if (mOnPreparedListener != null) {
312                mOnPreparedListener.onPrepared(mMediaPlayer);
313            }
314            if (mMediaController != null) {
315                mMediaController.setEnabled(true);
316            }
317            mVideoWidth = mp.getVideoWidth();
318            mVideoHeight = mp.getVideoHeight();
319
320            int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
321            if (seekToPosition != 0) {
322                seekTo(seekToPosition);
323            }
324            if (mVideoWidth != 0 && mVideoHeight != 0) {
325                //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
326                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
327                if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
328                    // We didn't actually change the size (it was already at the size
329                    // we need), so we won't get a "surface changed" callback, so
330                    // start the video here instead of in the callback.
331                    if (mTargetState == STATE_PLAYING) {
332                        start();
333                        if (mMediaController != null) {
334                            mMediaController.show();
335                        }
336                    } else if (!isPlaying() &&
337                               (seekToPosition != 0 || getCurrentPosition() > 0)) {
338                       if (mMediaController != null) {
339                           // Show the media controls when we're paused into a video and make 'em stick.
340                           mMediaController.show(0);
341                       }
342                   }
343                }
344            } else {
345                // We don't know the video size yet, but should start anyway.
346                // The video size might be reported to us later.
347                if (mTargetState == STATE_PLAYING) {
348                    start();
349                }
350            }
351        }
352    };
353
354    private MediaPlayer.OnCompletionListener mCompletionListener =
355        new MediaPlayer.OnCompletionListener() {
356        public void onCompletion(MediaPlayer mp) {
357            mCurrentState = STATE_PLAYBACK_COMPLETED;
358            mTargetState = STATE_PLAYBACK_COMPLETED;
359            if (mMediaController != null) {
360                mMediaController.hide();
361            }
362            if (mOnCompletionListener != null) {
363                mOnCompletionListener.onCompletion(mMediaPlayer);
364            }
365        }
366    };
367
368    private MediaPlayer.OnErrorListener mErrorListener =
369        new MediaPlayer.OnErrorListener() {
370        public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
371            Log.d(TAG, "Error: " + framework_err + "," + impl_err);
372            mCurrentState = STATE_ERROR;
373            mTargetState = STATE_ERROR;
374            if (mMediaController != null) {
375                mMediaController.hide();
376            }
377
378            /* If an error handler has been supplied, use it and finish. */
379            if (mOnErrorListener != null) {
380                if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
381                    return true;
382                }
383            }
384
385            /* Otherwise, pop up an error dialog so the user knows that
386             * something bad has happened. Only try and pop up the dialog
387             * if we're attached to a window. When we're going away and no
388             * longer have a window, don't bother showing the user an error.
389             */
390            if (getWindowToken() != null) {
391                Resources r = mContext.getResources();
392                int messageId;
393
394                if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
395                    messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
396                } else {
397                    messageId = com.android.internal.R.string.VideoView_error_text_unknown;
398                }
399
400                new AlertDialog.Builder(mContext)
401                        .setMessage(messageId)
402                        .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
403                                new DialogInterface.OnClickListener() {
404                                    public void onClick(DialogInterface dialog, int whichButton) {
405                                        /* If we get here, there is no onError listener, so
406                                         * at least inform them that the video is over.
407                                         */
408                                        if (mOnCompletionListener != null) {
409                                            mOnCompletionListener.onCompletion(mMediaPlayer);
410                                        }
411                                    }
412                                })
413                        .setCancelable(false)
414                        .show();
415            }
416            return true;
417        }
418    };
419
420    private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
421        new MediaPlayer.OnBufferingUpdateListener() {
422        public void onBufferingUpdate(MediaPlayer mp, int percent) {
423            mCurrentBufferPercentage = percent;
424        }
425    };
426
427    /**
428     * Register a callback to be invoked when the media file
429     * is loaded and ready to go.
430     *
431     * @param l The callback that will be run
432     */
433    public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
434    {
435        mOnPreparedListener = l;
436    }
437
438    /**
439     * Register a callback to be invoked when the end of a media file
440     * has been reached during playback.
441     *
442     * @param l The callback that will be run
443     */
444    public void setOnCompletionListener(OnCompletionListener l)
445    {
446        mOnCompletionListener = l;
447    }
448
449    /**
450     * Register a callback to be invoked when an error occurs
451     * during playback or setup.  If no listener is specified,
452     * or if the listener returned false, VideoView will inform
453     * the user of any errors.
454     *
455     * @param l The callback that will be run
456     */
457    public void setOnErrorListener(OnErrorListener l)
458    {
459        mOnErrorListener = l;
460    }
461
462    /**
463     * Register a callback to be invoked when an informational event
464     * occurs during playback or setup.
465     *
466     * @param l The callback that will be run
467     */
468    public void setOnInfoListener(OnInfoListener l) {
469        mOnInfoListener = l;
470    }
471
472    SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
473    {
474        public void surfaceChanged(SurfaceHolder holder, int format,
475                                    int w, int h)
476        {
477            mSurfaceWidth = w;
478            mSurfaceHeight = h;
479            boolean isValidState =  (mTargetState == STATE_PLAYING);
480            boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
481            if (mMediaPlayer != null && isValidState && hasValidSize) {
482                if (mSeekWhenPrepared != 0) {
483                    seekTo(mSeekWhenPrepared);
484                }
485                start();
486            }
487        }
488
489        public void surfaceCreated(SurfaceHolder holder)
490        {
491            mSurfaceHolder = holder;
492            openVideo();
493        }
494
495        public void surfaceDestroyed(SurfaceHolder holder)
496        {
497            // after we return from this we can't use the surface any more
498            mSurfaceHolder = null;
499            if (mMediaController != null) mMediaController.hide();
500            release(true);
501        }
502    };
503
504    /*
505     * release the media player in any state
506     */
507    private void release(boolean cleartargetstate) {
508        if (mMediaPlayer != null) {
509            mMediaPlayer.reset();
510            mMediaPlayer.release();
511            mMediaPlayer = null;
512            mCurrentState = STATE_IDLE;
513            if (cleartargetstate) {
514                mTargetState  = STATE_IDLE;
515            }
516        }
517    }
518
519    @Override
520    public boolean onTouchEvent(MotionEvent ev) {
521        if (isInPlaybackState() && mMediaController != null) {
522            toggleMediaControlsVisiblity();
523        }
524        return false;
525    }
526
527    @Override
528    public boolean onTrackballEvent(MotionEvent ev) {
529        if (isInPlaybackState() && mMediaController != null) {
530            toggleMediaControlsVisiblity();
531        }
532        return false;
533    }
534
535    @Override
536    public boolean onKeyDown(int keyCode, KeyEvent event)
537    {
538        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
539                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
540                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
541                                     keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
542                                     keyCode != KeyEvent.KEYCODE_MENU &&
543                                     keyCode != KeyEvent.KEYCODE_CALL &&
544                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
545        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
546            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
547                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
548                if (mMediaPlayer.isPlaying()) {
549                    pause();
550                    mMediaController.show();
551                } else {
552                    start();
553                    mMediaController.hide();
554                }
555                return true;
556            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
557                if (!mMediaPlayer.isPlaying()) {
558                    start();
559                    mMediaController.hide();
560                }
561                return true;
562            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
563                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
564                if (mMediaPlayer.isPlaying()) {
565                    pause();
566                    mMediaController.show();
567                }
568                return true;
569            } else {
570                toggleMediaControlsVisiblity();
571            }
572        }
573
574        return super.onKeyDown(keyCode, event);
575    }
576
577    private void toggleMediaControlsVisiblity() {
578        if (mMediaController.isShowing()) {
579            mMediaController.hide();
580        } else {
581            mMediaController.show();
582        }
583    }
584
585    public void start() {
586        if (isInPlaybackState()) {
587            mMediaPlayer.start();
588            mCurrentState = STATE_PLAYING;
589        }
590        mTargetState = STATE_PLAYING;
591    }
592
593    public void pause() {
594        if (isInPlaybackState()) {
595            if (mMediaPlayer.isPlaying()) {
596                mMediaPlayer.pause();
597                mCurrentState = STATE_PAUSED;
598            }
599        }
600        mTargetState = STATE_PAUSED;
601    }
602
603    public void suspend() {
604        release(false);
605    }
606
607    public void resume() {
608        openVideo();
609    }
610
611    // cache duration as mDuration for faster access
612    public int getDuration() {
613        if (isInPlaybackState()) {
614            if (mDuration > 0) {
615                return mDuration;
616            }
617            mDuration = mMediaPlayer.getDuration();
618            return mDuration;
619        }
620        mDuration = -1;
621        return mDuration;
622    }
623
624    public int getCurrentPosition() {
625        if (isInPlaybackState()) {
626            return mMediaPlayer.getCurrentPosition();
627        }
628        return 0;
629    }
630
631    public void seekTo(int msec) {
632        if (isInPlaybackState()) {
633            mMediaPlayer.seekTo(msec);
634            mSeekWhenPrepared = 0;
635        } else {
636            mSeekWhenPrepared = msec;
637        }
638    }
639
640    public boolean isPlaying() {
641        return isInPlaybackState() && mMediaPlayer.isPlaying();
642    }
643
644    public int getBufferPercentage() {
645        if (mMediaPlayer != null) {
646            return mCurrentBufferPercentage;
647        }
648        return 0;
649    }
650
651    private boolean isInPlaybackState() {
652        return (mMediaPlayer != null &&
653                mCurrentState != STATE_ERROR &&
654                mCurrentState != STATE_IDLE &&
655                mCurrentState != STATE_PREPARING);
656    }
657
658    public boolean canPause() {
659        return mCanPause;
660    }
661
662    public boolean canSeekBackward() {
663        return mCanSeekBack;
664    }
665
666    public boolean canSeekForward() {
667        return mCanSeekForward;
668    }
669}
670