1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser;
6
7import android.app.Activity;
8import android.app.AlertDialog;
9import android.content.Context;
10import android.content.ContextWrapper;
11import android.content.DialogInterface;
12import android.graphics.Point;
13import android.provider.Settings;
14import android.util.Log;
15import android.view.Display;
16import android.view.Gravity;
17import android.view.KeyEvent;
18import android.view.Surface;
19import android.view.SurfaceHolder;
20import android.view.SurfaceView;
21import android.view.View;
22import android.view.ViewGroup;
23import android.view.WindowManager;
24import android.widget.FrameLayout;
25import android.widget.LinearLayout;
26import android.widget.ProgressBar;
27import android.widget.TextView;
28
29import org.chromium.base.CalledByNative;
30import org.chromium.base.JNINamespace;
31import org.chromium.base.ThreadUtils;
32import org.chromium.ui.base.ViewAndroid;
33import org.chromium.ui.base.ViewAndroidDelegate;
34import org.chromium.ui.base.WindowAndroid;
35
36/**
37 * This class implements accelerated fullscreen video playback using surface view.
38 */
39@JNINamespace("content")
40public class ContentVideoView extends FrameLayout
41        implements SurfaceHolder.Callback, ViewAndroidDelegate {
42
43    private static final String TAG = "ContentVideoView";
44
45    /* Do not change these values without updating their counterparts
46     * in include/media/mediaplayer.h!
47     */
48    private static final int MEDIA_NOP = 0; // interface test message
49    private static final int MEDIA_PREPARED = 1;
50    private static final int MEDIA_PLAYBACK_COMPLETE = 2;
51    private static final int MEDIA_BUFFERING_UPDATE = 3;
52    private static final int MEDIA_SEEK_COMPLETE = 4;
53    private static final int MEDIA_SET_VIDEO_SIZE = 5;
54    private static final int MEDIA_ERROR = 100;
55    private static final int MEDIA_INFO = 200;
56
57    /**
58     * Keep these error codes in sync with the code we defined in
59     * MediaPlayerListener.java.
60     */
61    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
62    public static final int MEDIA_ERROR_INVALID_CODE = 3;
63
64    // all possible internal states
65    private static final int STATE_ERROR              = -1;
66    private static final int STATE_IDLE               = 0;
67    private static final int STATE_PLAYING            = 1;
68    private static final int STATE_PAUSED             = 2;
69    private static final int STATE_PLAYBACK_COMPLETED = 3;
70
71    private SurfaceHolder mSurfaceHolder;
72    private int mVideoWidth;
73    private int mVideoHeight;
74    private int mDuration;
75
76    // Native pointer to C++ ContentVideoView object.
77    private long mNativeContentVideoView;
78
79    // webkit should have prepared the media
80    private int mCurrentState = STATE_IDLE;
81
82    // Strings for displaying media player errors
83    private String mPlaybackErrorText;
84    private String mUnknownErrorText;
85    private String mErrorButton;
86    private String mErrorTitle;
87    private String mVideoLoadingText;
88
89    // This view will contain the video.
90    private VideoSurfaceView mVideoSurfaceView;
91
92    // Progress view when the video is loading.
93    private View mProgressView;
94
95    // The ViewAndroid is used to keep screen on during video playback.
96    private ViewAndroid mViewAndroid;
97
98    private final ContentVideoViewClient mClient;
99
100    private boolean mInitialOrientation;
101    private boolean mPossibleAccidentalChange;
102    private boolean mUmaRecorded;
103    private long mOrientationChangedTime;
104    private long mPlaybackStartTime;
105
106    private class VideoSurfaceView extends SurfaceView {
107
108        public VideoSurfaceView(Context context) {
109            super(context);
110        }
111
112        @Override
113        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
114            // set the default surface view size to (1, 1) so that it won't block
115            // the infobar. (0, 0) is not a valid size for surface view.
116            int width = 1;
117            int height = 1;
118            if (mVideoWidth > 0 && mVideoHeight > 0) {
119                width = getDefaultSize(mVideoWidth, widthMeasureSpec);
120                height = getDefaultSize(mVideoHeight, heightMeasureSpec);
121                if (mVideoWidth * height  > width * mVideoHeight) {
122                    height = width * mVideoHeight / mVideoWidth;
123                } else if (mVideoWidth * height  < width * mVideoHeight) {
124                    width = height * mVideoWidth / mVideoHeight;
125                }
126            }
127            if (mUmaRecorded) {
128                // If we have never switched orientation, record the orientation
129                // time.
130                if (mPlaybackStartTime == mOrientationChangedTime) {
131                   if (isOrientationPortrait() != mInitialOrientation) {
132                       mOrientationChangedTime = System.currentTimeMillis();
133                   }
134                } else {
135                   // if user quickly switched the orientation back and force, don't
136                   // count it in UMA.
137                   if (!mPossibleAccidentalChange &&
138                           isOrientationPortrait() == mInitialOrientation &&
139                           System.currentTimeMillis() - mOrientationChangedTime < 5000) {
140                       mPossibleAccidentalChange = true;
141                   }
142                }
143            }
144            setMeasuredDimension(width, height);
145        }
146    }
147
148    private static class ProgressView extends LinearLayout {
149
150        private final ProgressBar mProgressBar;
151        private final TextView mTextView;
152
153        public ProgressView(Context context, String videoLoadingText) {
154            super(context);
155            setOrientation(LinearLayout.VERTICAL);
156            setLayoutParams(new LinearLayout.LayoutParams(
157                    LinearLayout.LayoutParams.WRAP_CONTENT,
158                    LinearLayout.LayoutParams.WRAP_CONTENT));
159            mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
160            mTextView = new TextView(context);
161            mTextView.setText(videoLoadingText);
162            addView(mProgressBar);
163            addView(mTextView);
164        }
165    }
166
167    private final Runnable mExitFullscreenRunnable = new Runnable() {
168        @Override
169        public void run() {
170            exitFullscreen(true);
171        }
172    };
173
174    protected ContentVideoView(Context context, long nativeContentVideoView,
175            ContentVideoViewClient client) {
176        super(context);
177        mNativeContentVideoView = nativeContentVideoView;
178        mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
179        mClient = client;
180        mUmaRecorded = false;
181        mPossibleAccidentalChange = false;
182        initResources(context);
183        mVideoSurfaceView = new VideoSurfaceView(context);
184        showContentVideoView();
185        setVisibility(View.VISIBLE);
186    }
187
188    protected ContentVideoViewClient getContentVideoViewClient() {
189        return mClient;
190    }
191
192    private void initResources(Context context) {
193        if (mPlaybackErrorText != null) return;
194        mPlaybackErrorText = context.getString(
195                org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
196        mUnknownErrorText = context.getString(
197                org.chromium.content.R.string.media_player_error_text_unknown);
198        mErrorButton = context.getString(
199                org.chromium.content.R.string.media_player_error_button);
200        mErrorTitle = context.getString(
201                org.chromium.content.R.string.media_player_error_title);
202        mVideoLoadingText = context.getString(
203                org.chromium.content.R.string.media_player_loading_video);
204    }
205
206    protected void showContentVideoView() {
207        mVideoSurfaceView.getHolder().addCallback(this);
208        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
209                ViewGroup.LayoutParams.WRAP_CONTENT,
210                ViewGroup.LayoutParams.WRAP_CONTENT,
211                Gravity.CENTER));
212
213        mProgressView = mClient.getVideoLoadingProgressView();
214        if (mProgressView == null) {
215            mProgressView = new ProgressView(getContext(), mVideoLoadingText);
216        }
217        this.addView(mProgressView, new FrameLayout.LayoutParams(
218                ViewGroup.LayoutParams.WRAP_CONTENT,
219                ViewGroup.LayoutParams.WRAP_CONTENT,
220                Gravity.CENTER));
221    }
222
223    protected SurfaceView getSurfaceView() {
224        return mVideoSurfaceView;
225    }
226
227    @CalledByNative
228    public void onMediaPlayerError(int errorType) {
229        Log.d(TAG, "OnMediaPlayerError: " + errorType);
230        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
231            return;
232        }
233
234        // Ignore some invalid error codes.
235        if (errorType == MEDIA_ERROR_INVALID_CODE) {
236            return;
237        }
238
239        mCurrentState = STATE_ERROR;
240
241        if (!isActivityContext(getContext())) {
242            Log.w(TAG, "Unable to show alert dialog because it requires an activity context");
243            return;
244        }
245
246        /* Pop up an error dialog so the user knows that
247         * something bad has happened. Only try and pop up the dialog
248         * if we're attached to a window. When we're going away and no
249         * longer have a window, don't bother showing the user an error.
250         *
251         * TODO(qinmin): We need to review whether this Dialog is OK with
252         * the rest of the browser UI elements.
253         */
254        if (getWindowToken() != null) {
255            String message;
256
257            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
258                message = mPlaybackErrorText;
259            } else {
260                message = mUnknownErrorText;
261            }
262
263            try {
264                new AlertDialog.Builder(getContext())
265                    .setTitle(mErrorTitle)
266                    .setMessage(message)
267                    .setPositiveButton(mErrorButton,
268                            new DialogInterface.OnClickListener() {
269                        @Override
270                        public void onClick(DialogInterface dialog, int whichButton) {
271                            /* Inform that the video is over.
272                             */
273                            onCompletion();
274                        }
275                    })
276                    .setCancelable(false)
277                    .show();
278            } catch (RuntimeException e) {
279                Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
280            }
281        }
282    }
283
284    @CalledByNative
285    private void onVideoSizeChanged(int width, int height) {
286        mVideoWidth = width;
287        mVideoHeight = height;
288        // This will trigger the SurfaceView.onMeasure() call.
289        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
290    }
291
292    @CalledByNative
293    protected void onBufferingUpdate(int percent) {
294    }
295
296    @CalledByNative
297    private void onPlaybackComplete() {
298        onCompletion();
299    }
300
301    @CalledByNative
302    protected void onUpdateMediaMetadata(
303            int videoWidth,
304            int videoHeight,
305            int duration,
306            boolean canPause,
307            boolean canSeekBack,
308            boolean canSeekForward) {
309        mDuration = duration;
310        mProgressView.setVisibility(View.GONE);
311        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
312        onVideoSizeChanged(videoWidth, videoHeight);
313        if (mUmaRecorded) return;
314        try {
315            if (Settings.System.getInt(getContext().getContentResolver(),
316                    Settings.System.ACCELEROMETER_ROTATION) == 0) {
317                return;
318            }
319        } catch (Settings.SettingNotFoundException e) {
320            return;
321        }
322        mInitialOrientation = isOrientationPortrait();
323        mUmaRecorded = true;
324        mPlaybackStartTime = System.currentTimeMillis();
325        mOrientationChangedTime = mPlaybackStartTime;
326        nativeRecordFullscreenPlayback(
327                mNativeContentVideoView, videoHeight > videoWidth, mInitialOrientation);
328    }
329
330    @Override
331    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
332    }
333
334    @Override
335    public void surfaceCreated(SurfaceHolder holder) {
336        mSurfaceHolder = holder;
337        openVideo();
338    }
339
340    @Override
341    public void surfaceDestroyed(SurfaceHolder holder) {
342        if (mNativeContentVideoView != 0) {
343            nativeSetSurface(mNativeContentVideoView, null);
344        }
345        mSurfaceHolder = null;
346        post(mExitFullscreenRunnable);
347    }
348
349    @CalledByNative
350    protected void openVideo() {
351        if (mSurfaceHolder != null) {
352            mCurrentState = STATE_IDLE;
353            if (mNativeContentVideoView != 0) {
354                nativeRequestMediaMetadata(mNativeContentVideoView);
355                nativeSetSurface(mNativeContentVideoView,
356                        mSurfaceHolder.getSurface());
357            }
358        }
359    }
360
361    protected void onCompletion() {
362        mCurrentState = STATE_PLAYBACK_COMPLETED;
363    }
364
365
366    protected boolean isInPlaybackState() {
367        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
368    }
369
370    protected void start() {
371        if (isInPlaybackState()) {
372            if (mNativeContentVideoView != 0) {
373                nativePlay(mNativeContentVideoView);
374            }
375            mCurrentState = STATE_PLAYING;
376        }
377    }
378
379    protected void pause() {
380        if (isInPlaybackState()) {
381            if (isPlaying()) {
382                if (mNativeContentVideoView != 0) {
383                    nativePause(mNativeContentVideoView);
384                }
385                mCurrentState = STATE_PAUSED;
386            }
387        }
388    }
389
390    // cache duration as mDuration for faster access
391    protected int getDuration() {
392        if (isInPlaybackState()) {
393            if (mDuration > 0) {
394                return mDuration;
395            }
396            if (mNativeContentVideoView != 0) {
397                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
398            } else {
399                mDuration = 0;
400            }
401            return mDuration;
402        }
403        mDuration = -1;
404        return mDuration;
405    }
406
407    protected int getCurrentPosition() {
408        if (isInPlaybackState() && mNativeContentVideoView != 0) {
409            return nativeGetCurrentPosition(mNativeContentVideoView);
410        }
411        return 0;
412    }
413
414    protected void seekTo(int msec) {
415        if (mNativeContentVideoView != 0) {
416            nativeSeekTo(mNativeContentVideoView, msec);
417        }
418    }
419
420    public boolean isPlaying() {
421        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
422    }
423
424    @CalledByNative
425    private static ContentVideoView createContentVideoView(
426            Context context, long nativeContentVideoView, ContentVideoViewClient client) {
427        ThreadUtils.assertOnUiThread();
428        ContentVideoView videoView = new ContentVideoView(context, nativeContentVideoView, client);
429        if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
430            return videoView;
431        }
432        return null;
433    }
434
435    private static boolean isActivityContext(Context context) {
436        // Only retrieve the base context if the supplied context is a ContextWrapper but not
437        // an Activity, given that Activity is already a subclass of ContextWrapper.
438        if (context instanceof ContextWrapper && !(context instanceof Activity)) {
439            context = ((ContextWrapper) context).getBaseContext();
440            return isActivityContext(context);
441        }
442        return context instanceof Activity;
443    }
444
445    public void removeSurfaceView() {
446        removeView(mVideoSurfaceView);
447        removeView(mProgressView);
448        mVideoSurfaceView = null;
449        mProgressView = null;
450    }
451
452    public void exitFullscreen(boolean relaseMediaPlayer) {
453        destroyContentVideoView(false);
454        if (mNativeContentVideoView != 0) {
455            if (mUmaRecorded && !mPossibleAccidentalChange) {
456                long currentTime = System.currentTimeMillis();
457                long timeBeforeOrientationChange = mOrientationChangedTime - mPlaybackStartTime;
458                long timeAfterOrientationChange = currentTime - mOrientationChangedTime;
459                if (timeBeforeOrientationChange == 0) {
460                    timeBeforeOrientationChange = timeAfterOrientationChange;
461                    timeAfterOrientationChange = 0;
462                }
463                nativeRecordExitFullscreenPlayback(mNativeContentVideoView, mInitialOrientation,
464                        timeBeforeOrientationChange, timeAfterOrientationChange);
465            }
466            nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
467            mNativeContentVideoView = 0;
468        }
469    }
470
471    @CalledByNative
472    private void onExitFullscreen() {
473        exitFullscreen(false);
474    }
475
476    /**
477     * This method shall only be called by native and exitFullscreen,
478     * To exit fullscreen, use exitFullscreen in Java.
479     */
480    @CalledByNative
481    protected void destroyContentVideoView(boolean nativeViewDestroyed) {
482        if (mVideoSurfaceView != null) {
483            removeSurfaceView();
484            setVisibility(View.GONE);
485
486            // To prevent re-entrance, call this after removeSurfaceView.
487            mClient.onDestroyContentVideoView();
488        }
489        if (nativeViewDestroyed) {
490            mNativeContentVideoView = 0;
491        }
492    }
493
494    public static ContentVideoView getContentVideoView() {
495        return nativeGetSingletonJavaContentVideoView();
496    }
497
498    @Override
499    public boolean onKeyUp(int keyCode, KeyEvent event) {
500        if (keyCode == KeyEvent.KEYCODE_BACK) {
501            exitFullscreen(false);
502            return true;
503        }
504        return super.onKeyUp(keyCode, event);
505    }
506
507    @Override
508    public View acquireAnchorView() {
509        View anchorView = new View(getContext());
510        addView(anchorView);
511        return anchorView;
512    }
513
514    @Override
515    public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
516        Log.e(TAG, "setAnchorViewPosition isn't implemented");
517    }
518
519    @Override
520    public void releaseAnchorView(View anchorView) {
521        removeView(anchorView);
522    }
523
524    @CalledByNative
525    private long getNativeViewAndroid() {
526        return mViewAndroid.getNativePointer();
527    }
528
529    private boolean isOrientationPortrait() {
530        Context context = getContext();
531        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
532        Display display = manager.getDefaultDisplay();
533        Point outputSize = new Point(0, 0);
534        display.getSize(outputSize);
535        return outputSize.x <= outputSize.y;
536    }
537
538    private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
539    private native void nativeExitFullscreen(long nativeContentVideoView,
540            boolean relaseMediaPlayer);
541    private native int nativeGetCurrentPosition(long nativeContentVideoView);
542    private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
543    private native void nativeRequestMediaMetadata(long nativeContentVideoView);
544    private native int nativeGetVideoWidth(long nativeContentVideoView);
545    private native int nativeGetVideoHeight(long nativeContentVideoView);
546    private native boolean nativeIsPlaying(long nativeContentVideoView);
547    private native void nativePause(long nativeContentVideoView);
548    private native void nativePlay(long nativeContentVideoView);
549    private native void nativeSeekTo(long nativeContentVideoView, int msec);
550    private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
551    private native void nativeRecordFullscreenPlayback(
552            long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait);
553    private native void nativeRecordExitFullscreenPlayback(
554            long nativeContentVideoView, boolean isOrientationPortrait,
555            long playbackDurationBeforeOrientationChange,
556            long playbackDurationAfterOrientationChange);
557}
558