blob: c1fcd580b60e93feef765c730c98df2598648d79 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.MediaController;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.ThreadUtils;
import org.chromium.content.common.IChildProcessService;
import org.chromium.content.R;
@JNINamespace("content")
public class ContentVideoView
extends FrameLayout
implements ContentVideoViewControls.Delegate,
SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener {
private static final String TAG = "ContentVideoView";
/* Do not change these values without updating their counterparts
* in include/media/mediaplayer.h!
*/
private static final int MEDIA_NOP = 0; // interface test message
private static final int MEDIA_PREPARED = 1;
private static final int MEDIA_PLAYBACK_COMPLETE = 2;
private static final int MEDIA_BUFFERING_UPDATE = 3;
private static final int MEDIA_SEEK_COMPLETE = 4;
private static final int MEDIA_SET_VIDEO_SIZE = 5;
private static final int MEDIA_ERROR = 100;
private static final int MEDIA_INFO = 200;
/**
* Keep these error codes in sync with the code we defined in
* MediaPlayerListener.java.
*/
public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
public static final int MEDIA_ERROR_INVALID_CODE = 3;
// all possible internal states
private static final int STATE_ERROR = -1;
private static final int STATE_IDLE = 0;
private static final int STATE_PLAYING = 1;
private static final int STATE_PAUSED = 2;
private static final int STATE_PLAYBACK_COMPLETED = 3;
private SurfaceHolder mSurfaceHolder;
private int mVideoWidth;
private int mVideoHeight;
private int mCurrentBufferPercentage;
private int mDuration;
private ContentVideoViewControls mControls;
private boolean mCanPause;
private boolean mCanSeekBack;
private boolean mCanSeekForward;
// Native pointer to C++ ContentVideoView object.
private int mNativeContentVideoView;
// webkit should have prepared the media
private int mCurrentState = STATE_IDLE;
// Strings for displaying media player errors
private String mPlaybackErrorText;
private String mUnknownErrorText;
private String mErrorButton;
private String mErrorTitle;
private String mVideoLoadingText;
// This view will contain the video.
private VideoSurfaceView mVideoSurfaceView;
// Progress view when the video is loading.
private View mProgressView;
private Surface mSurface;
private ContentVideoViewClient mClient;
private class VideoSurfaceView extends SurfaceView {
public VideoSurfaceView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mVideoWidth == 0 && mVideoHeight == 0) {
setMeasuredDimension(1, 1);
return;
}
int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
if (mVideoWidth > 0 && mVideoHeight > 0) {
if ( mVideoWidth * height > width * mVideoHeight ) {
height = width * mVideoHeight / mVideoWidth;
} else if ( mVideoWidth * height < width * mVideoHeight ) {
width = height * mVideoWidth / mVideoHeight;
}
}
setMeasuredDimension(width, height);
}
}
private static class ProgressView extends LinearLayout {
private ProgressBar mProgressBar;
private TextView mTextView;
public ProgressView(Context context, String videoLoadingText) {
super(context);
setOrientation(LinearLayout.VERTICAL);
setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
mTextView = new TextView(context);
mTextView.setText(videoLoadingText);
addView(mProgressBar);
addView(mTextView);
}
}
private static class FullScreenControls implements ContentVideoViewControls {
View mVideoView;
MediaController mMediaController;
public FullScreenControls(Context context, View video) {
mMediaController = new MediaController(context);
mVideoView = video;
}
@Override
public void show() {
mMediaController.show();
if (mVideoView != null) {
mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
@Override
public void show(int timeout_ms) {
mMediaController.show(timeout_ms);
}
@Override
public void hide() {
if (mVideoView != null) {
mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
mMediaController.hide();
}
@Override
public boolean isShowing() {
return mMediaController.isShowing();
}
@Override
public void setEnabled(boolean enabled) {
mMediaController.setEnabled(enabled);
}
@Override
public void setDelegate(Delegate delegate) {
mMediaController.setMediaPlayer(delegate);
}
@Override
public void setAnchorView(View view) {
mMediaController.setAnchorView(view);
}
}
private Runnable mExitFullscreenRunnable = new Runnable() {
@Override
public void run() {
exitFullscreen(true);
}
};
private ContentVideoView(Context context, int nativeContentVideoView,
ContentVideoViewClient client) {
super(context);
mNativeContentVideoView = nativeContentVideoView;
mClient = client;
initResources(context);
mCurrentBufferPercentage = 0;
mVideoSurfaceView = new VideoSurfaceView(context);
setBackgroundColor(Color.BLACK);
showContentVideoView();
setVisibility(View.VISIBLE);
mClient.onShowCustomView(this);
}
private void initResources(Context context) {
if (mPlaybackErrorText != null) return;
mPlaybackErrorText = context.getString(
org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
mUnknownErrorText = context.getString(
org.chromium.content.R.string.media_player_error_text_unknown);
mErrorButton = context.getString(
org.chromium.content.R.string.media_player_error_button);
mErrorTitle = context.getString(
org.chromium.content.R.string.media_player_error_title);
mVideoLoadingText = context.getString(
org.chromium.content.R.string.media_player_loading_video);
}
private void showContentVideoView() {
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.CENTER);
this.addView(mVideoSurfaceView, layoutParams);
View progressView = mClient.getVideoLoadingProgressView();
if (progressView != null) {
mProgressView = progressView;
} else {
mProgressView = new ProgressView(getContext(), mVideoLoadingText);
}
this.addView(mProgressView, layoutParams);
mVideoSurfaceView.setZOrderOnTop(true);
mVideoSurfaceView.setOnKeyListener(this);
mVideoSurfaceView.setOnTouchListener(this);
mVideoSurfaceView.getHolder().addCallback(this);
mVideoSurfaceView.setFocusable(true);
mVideoSurfaceView.setFocusableInTouchMode(true);
mVideoSurfaceView.requestFocus();
}
@CalledByNative
public void onMediaPlayerError(int errorType) {
Log.d(TAG, "OnMediaPlayerError: " + errorType);
if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
return;
}
// Ignore some invalid error codes.
if (errorType == MEDIA_ERROR_INVALID_CODE) {
return;
}
mCurrentState = STATE_ERROR;
if (mControls != null) {
mControls.hide();
}
/* Pop up an error dialog so the user knows that
* something bad has happened. Only try and pop up the dialog
* if we're attached to a window. When we're going away and no
* longer have a window, don't bother showing the user an error.
*
* TODO(qinmin): We need to review whether this Dialog is OK with
* the rest of the browser UI elements.
*/
if (getWindowToken() != null) {
String message;
if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
message = mPlaybackErrorText;
} else {
message = mUnknownErrorText;
}
new AlertDialog.Builder(getContext())
.setTitle(mErrorTitle)
.setMessage(message)
.setPositiveButton(mErrorButton,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* Inform that the video is over.
*/
onCompletion();
}
})
.setCancelable(false)
.show();
}
}
@CalledByNative
private void onVideoSizeChanged(int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
if (mVideoWidth != 0 && mVideoHeight != 0) {
mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
}
}
@CalledByNative
private void onBufferingUpdate(int percent) {
mCurrentBufferPercentage = percent;
}
@CalledByNative
private void onPlaybackComplete() {
onCompletion();
}
@CalledByNative
private void onUpdateMediaMetadata(
int videoWidth,
int videoHeight,
int duration,
boolean canPause,
boolean canSeekBack,
boolean canSeekForward) {
mProgressView.setVisibility(View.GONE);
mDuration = duration;
mCanPause = canPause;
mCanSeekBack = canSeekBack;
mCanSeekForward = canSeekForward;
mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
if (mControls != null) {
mControls.setEnabled(true);
// If paused , should show the controller for ever.
if (isPlaying())
mControls.show();
else
mControls.show(0);
}
onVideoSizeChanged(videoWidth, videoHeight);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mVideoSurfaceView.setFocusable(true);
mVideoSurfaceView.setFocusableInTouchMode(true);
if (isInPlaybackState() && mControls != null) {
mControls.show();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mSurfaceHolder = holder;
openVideo();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mNativeContentVideoView != 0) {
nativeSetSurface(mNativeContentVideoView, null);
}
mSurfaceHolder = null;
post(mExitFullscreenRunnable);
}
private void setControls(ContentVideoViewControls controls) {
if (mControls != null) {
mControls.hide();
}
mControls = controls;
attachControls();
}
private void attachControls() {
if (mControls != null) {
mControls.setDelegate(this);
mControls.setAnchorView(mVideoSurfaceView);
mControls.setEnabled(false);
}
}
@CalledByNative
private void openVideo() {
if (mSurfaceHolder != null) {
mCurrentState = STATE_IDLE;
mCurrentBufferPercentage = 0;
ContentVideoViewControls controls = mClient.createControls();
if (controls == null) {
controls = new FullScreenControls(getContext(), this);
}
setControls(controls);
if (mNativeContentVideoView != 0) {
nativeUpdateMediaMetadata(mNativeContentVideoView);
nativeSetSurface(mNativeContentVideoView,
mSurfaceHolder.getSurface());
}
}
}
private void onCompletion() {
mCurrentState = STATE_PLAYBACK_COMPLETED;
if (mControls != null) {
mControls.hide();
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isInPlaybackState() && mControls != null &&
event.getAction() == MotionEvent.ACTION_DOWN) {
toggleMediaControlsVisiblity();
}
return true;
}
@Override
public boolean onTrackballEvent(MotionEvent ev) {
if (isInPlaybackState() && mControls != null) {
toggleMediaControlsVisiblity();
}
return false;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
keyCode != KeyEvent.KEYCODE_CALL &&
keyCode != KeyEvent.KEYCODE_MENU &&
keyCode != KeyEvent.KEYCODE_SEARCH &&
keyCode != KeyEvent.KEYCODE_ENDCALL;
if (isInPlaybackState() && isKeyCodeSupported && mControls != null) {
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
if (isPlaying()) {
pause();
mControls.show();
} else {
start();
mControls.hide();
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
if (!isPlaying()) {
start();
mControls.hide();
}
return true;
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
if (isPlaying()) {
pause();
mControls.show();
}
return true;
} else {
toggleMediaControlsVisiblity();
}
} else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
exitFullscreen(false);
return true;
} else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return super.onKeyDown(keyCode, event);
}
private void toggleMediaControlsVisiblity() {
if (mControls.isShowing()) {
mControls.hide();
} else {
mControls.show();
}
}
private boolean isInPlaybackState() {
return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
}
@Override
public void start() {
if (isInPlaybackState()) {
if (mNativeContentVideoView != 0) {
nativePlay(mNativeContentVideoView);
}
mCurrentState = STATE_PLAYING;
}
}
@Override
public void pause() {
if (isInPlaybackState()) {
if (isPlaying()) {
if (mNativeContentVideoView != 0) {
nativePause(mNativeContentVideoView);
}
mCurrentState = STATE_PAUSED;
}
}
}
// cache duration as mDuration for faster access
@Override
public int getDuration() {
if (isInPlaybackState()) {
if (mDuration > 0) {
return mDuration;
}
if (mNativeContentVideoView != 0) {
mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
} else {
mDuration = 0;
}
return mDuration;
}
mDuration = -1;
return mDuration;
}
@Override
public int getCurrentPosition() {
if (isInPlaybackState() && mNativeContentVideoView != 0) {
return nativeGetCurrentPosition(mNativeContentVideoView);
}
return 0;
}
@Override
public void seekTo(int msec) {
if (mNativeContentVideoView != 0) {
nativeSeekTo(mNativeContentVideoView, msec);
}
}
@Override
public boolean isPlaying() {
return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
}
@Override
public int getBufferPercentage() {
return mCurrentBufferPercentage;
}
@Override
public boolean canPause() {
return mCanPause;
}
@Override
public boolean canSeekBackward() {
return mCanSeekBack;
}
@Override
public boolean canSeekForward() {
return mCanSeekForward;
}
public int getAudioSessionId() {
return 0;
}
@CalledByNative
private static ContentVideoView createContentVideoView(
Context context, int nativeContentVideoView, ContentVideoViewClient client) {
ThreadUtils.assertOnUiThread();
// The context needs be Activity to create the ContentVideoView correctly.
if (!(context instanceof Activity)) {
Log.w(TAG, "Wrong type of context, can't create fullscreen video");
return null;
}
return new ContentVideoView(context, nativeContentVideoView, client);
}
private void removeControls() {
if (mControls != null) {
mControls.setEnabled(false);
mControls.hide();
mControls = null;
}
}
public void removeSurfaceView() {
removeView(mVideoSurfaceView);
removeView(mProgressView);
mVideoSurfaceView = null;
mProgressView = null;
}
public void exitFullscreen(boolean relaseMediaPlayer) {
destroyContentVideoView(false);
if (mNativeContentVideoView != 0) {
nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
mNativeContentVideoView = 0;
}
}
/**
* This method shall only be called by native and exitFullscreen,
* To exit fullscreen, use exitFullscreen in Java.
*/
@CalledByNative
private void destroyContentVideoView(boolean nativeViewDestroyed) {
if (mVideoSurfaceView != null) {
removeControls();
removeSurfaceView();
setVisibility(View.GONE);
// To prevent re-entrance, call this after removeSurfaceView.
mClient.onDestroyContentVideoView();
}
if (nativeViewDestroyed) {
mNativeContentVideoView = 0;
}
}
public static ContentVideoView getContentVideoView() {
return nativeGetSingletonJavaContentVideoView();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
exitFullscreen(false);
return true;
}
return super.onKeyDown(keyCode, event);
}
private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer);
private native int nativeGetCurrentPosition(int nativeContentVideoView);
private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView);
private native void nativeUpdateMediaMetadata(int nativeContentVideoView);
private native int nativeGetVideoWidth(int nativeContentVideoView);
private native int nativeGetVideoHeight(int nativeContentVideoView);
private native boolean nativeIsPlaying(int nativeContentVideoView);
private native void nativePause(int nativeContentVideoView);
private native void nativePlay(int nativeContentVideoView);
private native void nativeSeekTo(int nativeContentVideoView, int msec);
private native void nativeSetSurface(int nativeContentVideoView, Surface surface);
}