/* * 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); } } } }