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