blob: 9132c52727e366ded4121a75348a7b7cfd67e067 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.pictureinpicture.widget;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import android.transition.TransitionManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import com.example.android.pictureinpicture.R;
import java.io.IOException;
import java.lang.ref.WeakReference;
/**
* Provides video playback. There is nothing directly related to Picture-in-Picture here.
*
* <p>This is similar to {@link android.widget.VideoView}, but it comes with a custom control
* (play/pause, fast forward, and fast rewind).</p>
*/
public class MovieView extends RelativeLayout {
/**
* Monitors all events related to {@link MovieView}.
*/
public static abstract class MovieListener {
/**
* Called when the video is started or resumed.
*/
public void onMovieStarted() {
}
/**
* Called when the video is paused or finished.
*/
public void onMovieStopped() {
}
/**
* Called when this view should be minimized.
*/
public void onMovieMinimized() {
}
}
private static final String TAG = "MovieView";
/** The amount of time we are stepping forward or backward for fast-forward and fast-rewind. */
private static final int FAST_FORWARD_REWIND_INTERVAL = 5000; // ms
/** The amount of time until we fade out the controls. */
private static final int TIMEOUT_CONTROLS = 3000; // ms
/** Shows the video playback. */
private final SurfaceView mSurfaceView;
// Controls
private final ImageButton mToggle;
private final View mShade;
private final ImageButton mFastForward;
private final ImageButton mFastRewind;
private final ImageButton mMinimize;
/** This plays the video. This will be null when no video is set. */
MediaPlayer mMediaPlayer;
/** The resource ID for the video to play. */
@RawRes
private int mVideoResourceId;
/** Whether we adjust our view bounds or we fill the remaining area with black bars */
private boolean mAdjustViewBounds;
/** Handles timeout for media controls. */
TimeoutHandler mTimeoutHandler;
/** The listener for all the events we publish. */
MovieListener mMovieListener;
private int mSavedCurrentPosition;
public MovieView(Context context) {
this(context, null);
}
public MovieView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MovieView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setBackgroundColor(Color.BLACK);
// Inflate the content
inflate(context, R.layout.view_movie, this);
mSurfaceView = (SurfaceView) findViewById(R.id.surface);
mShade = findViewById(R.id.shade);
mToggle = (ImageButton) findViewById(R.id.toggle);
mFastForward = (ImageButton) findViewById(R.id.fast_forward);
mFastRewind = (ImageButton) findViewById(R.id.fast_rewind);
mMinimize = (ImageButton) findViewById(R.id.minimize);
// Attributes
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MovieView,
defStyleAttr, R.style.Widget_PictureInPicture_MovieView);
setVideoResourceId(a.getResourceId(R.styleable.MovieView_android_src, 0));
setAdjustViewBounds(a.getBoolean(R.styleable.MovieView_android_adjustViewBounds, false));
a.recycle();
// Bind view events
final OnClickListener listener = new OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.surface:
toggleControls();
break;
case R.id.toggle:
toggle();
break;
case R.id.fast_forward:
fastForward();
break;
case R.id.fast_rewind:
fastRewind();
break;
case R.id.minimize:
if (mMovieListener != null) {
mMovieListener.onMovieMinimized();
}
break;
}
// Start or reset the timeout to hide controls
if (mMediaPlayer != null) {
if (mTimeoutHandler == null) {
mTimeoutHandler = new TimeoutHandler(MovieView.this);
}
mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS);
if (mMediaPlayer.isPlaying()) {
mTimeoutHandler.sendEmptyMessageDelayed(
TimeoutHandler.MESSAGE_HIDE_CONTROLS, TIMEOUT_CONTROLS);
}
}
}
};
mSurfaceView.setOnClickListener(listener);
mToggle.setOnClickListener(listener);
mFastForward.setOnClickListener(listener);
mFastRewind.setOnClickListener(listener);
mMinimize.setOnClickListener(listener);
// Prepare video playback
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
openVideo(holder.getSurface());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Do nothing
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mMediaPlayer != null) {
mSavedCurrentPosition = mMediaPlayer.getCurrentPosition();
}
closeVideo();
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mMediaPlayer != null) {
final int videoWidth = mMediaPlayer.getVideoWidth();
final int videoHeight = mMediaPlayer.getVideoHeight();
if (videoWidth != 0 && videoHeight != 0) {
final float aspectRatio = (float) videoHeight / videoWidth;
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (mAdjustViewBounds) {
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
super.onMeasure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec((int) (width * aspectRatio),
MeasureSpec.EXACTLY));
} else if (widthMode != MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY) {
super.onMeasure(MeasureSpec.makeMeasureSpec((int) (height / aspectRatio),
MeasureSpec.EXACTLY), heightMeasureSpec);
} else {
super.onMeasure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec((int) (width * aspectRatio),
MeasureSpec.EXACTLY));
}
} else {
final float viewRatio = (float) height / width;
if (aspectRatio > viewRatio) {
int padding = (int) ((width - height / aspectRatio) / 2);
setPadding(padding, 0, padding, 0);
} else {
int padding = (int) ((height - width * aspectRatio) / 2);
setPadding(0, padding, 0, padding);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
return;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDetachedFromWindow() {
if (mTimeoutHandler != null) {
mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS);
mTimeoutHandler = null;
}
super.onDetachedFromWindow();
}
/**
* Sets the listener to monitor movie events.
*
* @param movieListener The listener to be set.
*/
public void setMovieListener(@Nullable MovieListener movieListener) {
mMovieListener = movieListener;
}
/**
* Sets the raw resource ID of video to play.
*
* @param id The raw resource ID.
*/
public void setVideoResourceId(@RawRes int id) {
if (id == mVideoResourceId) {
return;
}
mVideoResourceId = id;
Surface surface = mSurfaceView.getHolder().getSurface();
if (surface != null && surface.isValid()) {
closeVideo();
openVideo(surface);
}
}
public void setAdjustViewBounds(boolean adjustViewBounds) {
if (mAdjustViewBounds == adjustViewBounds) {
return;
}
mAdjustViewBounds = adjustViewBounds;
if (adjustViewBounds) {
setBackground(null);
} else {
setBackgroundColor(Color.BLACK);
}
requestLayout();
}
/**
* Shows all the controls.
*/
public void showControls() {
TransitionManager.beginDelayedTransition(this);
mShade.setVisibility(View.VISIBLE);
mToggle.setVisibility(View.VISIBLE);
mFastForward.setVisibility(View.VISIBLE);
mFastRewind.setVisibility(View.VISIBLE);
mMinimize.setVisibility(View.VISIBLE);
}
/**
* Hides all the controls.
*/
public void hideControls() {
TransitionManager.beginDelayedTransition(this);
mShade.setVisibility(View.INVISIBLE);
mToggle.setVisibility(View.INVISIBLE);
mFastForward.setVisibility(View.INVISIBLE);
mFastRewind.setVisibility(View.INVISIBLE);
mMinimize.setVisibility(View.INVISIBLE);
}
/**
* Fast-forward the video.
*/
public void fastForward() {
if (mMediaPlayer == null) {
return;
}
mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() + FAST_FORWARD_REWIND_INTERVAL);
}
/**
* Fast-rewind the video.
*/
public void fastRewind() {
if (mMediaPlayer == null) {
return;
}
mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() - FAST_FORWARD_REWIND_INTERVAL);
}
public boolean isPlaying() {
return mMediaPlayer != null && mMediaPlayer.isPlaying();
}
public void play() {
if (mMediaPlayer == null) {
return;
}
mMediaPlayer.start();
adjustToggleState();
setKeepScreenOn(true);
if (mMovieListener != null) {
mMovieListener.onMovieStarted();
}
}
public void pause() {
if (mMediaPlayer == null) {
return;
}
mMediaPlayer.pause();
adjustToggleState();
setKeepScreenOn(false);
if (mMovieListener != null) {
mMovieListener.onMovieStopped();
}
}
void openVideo(Surface surface) {
if (mVideoResourceId == 0) {
return;
}
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setSurface(surface);
try (AssetFileDescriptor fd = getResources().openRawResourceFd(mVideoResourceId)) {
mMediaPlayer.setDataSource(fd);
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
// Adjust the aspect ratio of this view
requestLayout();
if (mSavedCurrentPosition > 0) {
mediaPlayer.seekTo(mSavedCurrentPosition);
mSavedCurrentPosition = 0;
} else {
// Start automatically
play();
}
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
adjustToggleState();
setKeepScreenOn(false);
if (mMovieListener != null) {
mMovieListener.onMovieStopped();
}
}
});
mMediaPlayer.prepare();
} catch (IOException e) {
Log.e(TAG, "Failed to open video", e);
}
}
void closeVideo() {
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
}
}
void toggle() {
if (mMediaPlayer == null) {
return;
}
if (mMediaPlayer.isPlaying()) {
pause();
} else {
play();
}
}
void toggleControls() {
if (mShade.getVisibility() == View.VISIBLE) {
hideControls();
} else {
showControls();
}
}
void adjustToggleState() {
if (mMediaPlayer == null || mMediaPlayer.isPlaying()) {
mToggle.setContentDescription(getResources().getString(R.string.pause));
mToggle.setImageResource(R.drawable.ic_pause_64dp);
} else {
mToggle.setContentDescription(getResources().getString(R.string.play));
mToggle.setImageResource(R.drawable.ic_play_arrow_64dp);
}
}
private static class TimeoutHandler extends Handler {
static final int MESSAGE_HIDE_CONTROLS = 1;
private final WeakReference<MovieView> mMovieViewRef;
TimeoutHandler(MovieView view) {
mMovieViewRef = new WeakReference<>(view);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_HIDE_CONTROLS:
MovieView movieView = mMovieViewRef.get();
if (movieView != null) {
movieView.hideControls();
}
break;
default:
super.handleMessage(msg);
}
}
}
}