1 /* 2 * Copyright (C) 2017 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 17 package com.example.android.pictureinpicture.widget; 18 19 import android.content.Context; 20 import android.content.res.AssetFileDescriptor; 21 import android.content.res.TypedArray; 22 import android.graphics.Color; 23 import android.media.MediaPlayer; 24 import android.os.Handler; 25 import android.os.Message; 26 import android.support.annotation.Nullable; 27 import android.support.annotation.RawRes; 28 import android.transition.TransitionManager; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.view.Surface; 32 import android.view.SurfaceHolder; 33 import android.view.SurfaceView; 34 import android.view.View; 35 import android.widget.ImageButton; 36 import android.widget.RelativeLayout; 37 38 import com.example.android.pictureinpicture.R; 39 40 import java.io.IOException; 41 import java.lang.ref.WeakReference; 42 43 /** 44 * Provides video playback. There is nothing directly related to Picture-in-Picture here. 45 * 46 * <p>This is similar to {@link android.widget.VideoView}, but it comes with a custom control 47 * (play/pause, fast forward, and fast rewind). 48 */ 49 public class MovieView extends RelativeLayout { 50 51 /** Monitors all events related to {@link MovieView}. */ 52 public abstract static class MovieListener { 53 54 /** Called when the video is started or resumed. */ 55 public void onMovieStarted() {} 56 57 /** Called when the video is paused or finished. */ 58 public void onMovieStopped() {} 59 60 /** Called when this view should be minimized. */ 61 public void onMovieMinimized() {} 62 } 63 64 private static final String TAG = "MovieView"; 65 66 /** The amount of time we are stepping forward or backward for fast-forward and fast-rewind. */ 67 private static final int FAST_FORWARD_REWIND_INTERVAL = 5000; // ms 68 69 /** The amount of time until we fade out the controls. */ 70 private static final int TIMEOUT_CONTROLS = 3000; // ms 71 72 /** Shows the video playback. */ 73 private final SurfaceView mSurfaceView; 74 75 // Controls 76 private final ImageButton mToggle; 77 private final View mShade; 78 private final ImageButton mFastForward; 79 private final ImageButton mFastRewind; 80 private final ImageButton mMinimize; 81 82 /** This plays the video. This will be null when no video is set. */ 83 MediaPlayer mMediaPlayer; 84 85 /** The resource ID for the video to play. */ 86 @RawRes private int mVideoResourceId; 87 88 /** Whether we adjust our view bounds or we fill the remaining area with black bars */ 89 private boolean mAdjustViewBounds; 90 91 /** Handles timeout for media controls. */ 92 TimeoutHandler mTimeoutHandler; 93 94 /** The listener for all the events we publish. */ 95 MovieListener mMovieListener; 96 97 private int mSavedCurrentPosition; 98 99 public MovieView(Context context) { 100 this(context, null); 101 } 102 103 public MovieView(Context context, AttributeSet attrs) { 104 this(context, attrs, 0); 105 } 106 107 public MovieView(Context context, AttributeSet attrs, int defStyleAttr) { 108 super(context, attrs, defStyleAttr); 109 setBackgroundColor(Color.BLACK); 110 111 // Inflate the content 112 inflate(context, R.layout.view_movie, this); 113 mSurfaceView = findViewById(R.id.surface); 114 mShade = findViewById(R.id.shade); 115 mToggle = findViewById(R.id.toggle); 116 mFastForward = findViewById(R.id.fast_forward); 117 mFastRewind = findViewById(R.id.fast_rewind); 118 mMinimize = findViewById(R.id.minimize); 119 120 // Attributes 121 final TypedArray a = 122 context.obtainStyledAttributes( 123 attrs, 124 R.styleable.MovieView, 125 defStyleAttr, 126 R.style.Widget_PictureInPicture_MovieView); 127 setVideoResourceId(a.getResourceId(R.styleable.MovieView_android_src, 0)); 128 setAdjustViewBounds(a.getBoolean(R.styleable.MovieView_android_adjustViewBounds, false)); 129 a.recycle(); 130 131 // Bind view events 132 final OnClickListener listener = 133 new OnClickListener() { 134 @Override 135 public void onClick(View view) { 136 switch (view.getId()) { 137 case R.id.surface: 138 toggleControls(); 139 break; 140 case R.id.toggle: 141 toggle(); 142 break; 143 case R.id.fast_forward: 144 fastForward(); 145 break; 146 case R.id.fast_rewind: 147 fastRewind(); 148 break; 149 case R.id.minimize: 150 if (mMovieListener != null) { 151 mMovieListener.onMovieMinimized(); 152 } 153 break; 154 } 155 // Start or reset the timeout to hide controls 156 if (mMediaPlayer != null) { 157 if (mTimeoutHandler == null) { 158 mTimeoutHandler = new TimeoutHandler(MovieView.this); 159 } 160 mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS); 161 if (mMediaPlayer.isPlaying()) { 162 mTimeoutHandler.sendEmptyMessageDelayed( 163 TimeoutHandler.MESSAGE_HIDE_CONTROLS, TIMEOUT_CONTROLS); 164 } 165 } 166 } 167 }; 168 mSurfaceView.setOnClickListener(listener); 169 mToggle.setOnClickListener(listener); 170 mFastForward.setOnClickListener(listener); 171 mFastRewind.setOnClickListener(listener); 172 mMinimize.setOnClickListener(listener); 173 174 // Prepare video playback 175 mSurfaceView 176 .getHolder() 177 .addCallback( 178 new SurfaceHolder.Callback() { 179 @Override 180 public void surfaceCreated(SurfaceHolder holder) { 181 openVideo(holder.getSurface()); 182 } 183 184 @Override 185 public void surfaceChanged( 186 SurfaceHolder holder, int format, int width, int height) { 187 // Do nothing 188 } 189 190 @Override 191 public void surfaceDestroyed(SurfaceHolder holder) { 192 if (mMediaPlayer != null) { 193 mSavedCurrentPosition = mMediaPlayer.getCurrentPosition(); 194 } 195 closeVideo(); 196 } 197 }); 198 } 199 200 @Override 201 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 202 if (mMediaPlayer != null) { 203 final int videoWidth = mMediaPlayer.getVideoWidth(); 204 final int videoHeight = mMediaPlayer.getVideoHeight(); 205 if (videoWidth != 0 && videoHeight != 0) { 206 final float aspectRatio = (float) videoHeight / videoWidth; 207 final int width = MeasureSpec.getSize(widthMeasureSpec); 208 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 209 final int height = MeasureSpec.getSize(heightMeasureSpec); 210 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 211 if (mAdjustViewBounds) { 212 if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) { 213 super.onMeasure( 214 widthMeasureSpec, 215 MeasureSpec.makeMeasureSpec( 216 (int) (width * aspectRatio), MeasureSpec.EXACTLY)); 217 } else if (widthMode != MeasureSpec.EXACTLY 218 && heightMode == MeasureSpec.EXACTLY) { 219 super.onMeasure( 220 MeasureSpec.makeMeasureSpec( 221 (int) (height / aspectRatio), MeasureSpec.EXACTLY), 222 heightMeasureSpec); 223 } else { 224 super.onMeasure( 225 widthMeasureSpec, 226 MeasureSpec.makeMeasureSpec( 227 (int) (width * aspectRatio), MeasureSpec.EXACTLY)); 228 } 229 } else { 230 final float viewRatio = (float) height / width; 231 if (aspectRatio > viewRatio) { 232 int padding = (int) ((width - height / aspectRatio) / 2); 233 setPadding(padding, 0, padding, 0); 234 } else { 235 int padding = (int) ((height - width * aspectRatio) / 2); 236 setPadding(0, padding, 0, padding); 237 } 238 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 239 } 240 return; 241 } 242 } 243 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 244 } 245 246 @Override 247 protected void onDetachedFromWindow() { 248 if (mTimeoutHandler != null) { 249 mTimeoutHandler.removeMessages(TimeoutHandler.MESSAGE_HIDE_CONTROLS); 250 mTimeoutHandler = null; 251 } 252 super.onDetachedFromWindow(); 253 } 254 255 /** 256 * Sets the listener to monitor movie events. 257 * 258 * @param movieListener The listener to be set. 259 */ 260 public void setMovieListener(@Nullable MovieListener movieListener) { 261 mMovieListener = movieListener; 262 } 263 264 /** 265 * The raw resource id of the video to play. 266 * 267 * @return ID of the video resource. 268 */ 269 public int getVideoResourceId() { 270 return mVideoResourceId; 271 } 272 273 /** 274 * Sets the raw resource ID of video to play. 275 * 276 * @param id The raw resource ID. 277 */ 278 public void setVideoResourceId(@RawRes int id) { 279 if (id == mVideoResourceId) { 280 return; 281 } 282 mVideoResourceId = id; 283 Surface surface = mSurfaceView.getHolder().getSurface(); 284 if (surface != null && surface.isValid()) { 285 closeVideo(); 286 openVideo(surface); 287 } 288 } 289 290 public void setAdjustViewBounds(boolean adjustViewBounds) { 291 if (mAdjustViewBounds == adjustViewBounds) { 292 return; 293 } 294 mAdjustViewBounds = adjustViewBounds; 295 if (adjustViewBounds) { 296 setBackground(null); 297 } else { 298 setBackgroundColor(Color.BLACK); 299 } 300 requestLayout(); 301 } 302 303 /** Shows all the controls. */ 304 public void showControls() { 305 TransitionManager.beginDelayedTransition(this); 306 mShade.setVisibility(View.VISIBLE); 307 mToggle.setVisibility(View.VISIBLE); 308 mFastForward.setVisibility(View.VISIBLE); 309 mFastRewind.setVisibility(View.VISIBLE); 310 mMinimize.setVisibility(View.VISIBLE); 311 } 312 313 /** Hides all the controls. */ 314 public void hideControls() { 315 TransitionManager.beginDelayedTransition(this); 316 mShade.setVisibility(View.INVISIBLE); 317 mToggle.setVisibility(View.INVISIBLE); 318 mFastForward.setVisibility(View.INVISIBLE); 319 mFastRewind.setVisibility(View.INVISIBLE); 320 mMinimize.setVisibility(View.INVISIBLE); 321 } 322 323 /** Fast-forward the video. */ 324 public void fastForward() { 325 if (mMediaPlayer == null) { 326 return; 327 } 328 mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() + FAST_FORWARD_REWIND_INTERVAL); 329 } 330 331 /** Fast-rewind the video. */ 332 public void fastRewind() { 333 if (mMediaPlayer == null) { 334 return; 335 } 336 mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition() - FAST_FORWARD_REWIND_INTERVAL); 337 } 338 339 /** 340 * Returns the current position of the video. If the the player has not been created, then 341 * assumes the beginning of the video. 342 * 343 * @return The current position of the video. 344 */ 345 public int getCurrentPosition() { 346 if (mMediaPlayer == null) { 347 return 0; 348 } 349 return mMediaPlayer.getCurrentPosition(); 350 } 351 352 public boolean isPlaying() { 353 return mMediaPlayer != null && mMediaPlayer.isPlaying(); 354 } 355 356 public void play() { 357 if (mMediaPlayer == null) { 358 return; 359 } 360 mMediaPlayer.start(); 361 adjustToggleState(); 362 setKeepScreenOn(true); 363 if (mMovieListener != null) { 364 mMovieListener.onMovieStarted(); 365 } 366 } 367 368 public void pause() { 369 if (mMediaPlayer == null) { 370 adjustToggleState(); 371 return; 372 } 373 mMediaPlayer.pause(); 374 adjustToggleState(); 375 setKeepScreenOn(false); 376 if (mMovieListener != null) { 377 mMovieListener.onMovieStopped(); 378 } 379 } 380 381 void openVideo(Surface surface) { 382 if (mVideoResourceId == 0) { 383 return; 384 } 385 mMediaPlayer = new MediaPlayer(); 386 mMediaPlayer.setSurface(surface); 387 startVideo(); 388 } 389 390 /** Restarts playback of the video. */ 391 public void startVideo() { 392 mMediaPlayer.reset(); 393 try (AssetFileDescriptor fd = getResources().openRawResourceFd(mVideoResourceId)) { 394 mMediaPlayer.setDataSource(fd); 395 mMediaPlayer.setOnPreparedListener( 396 new MediaPlayer.OnPreparedListener() { 397 @Override 398 public void onPrepared(MediaPlayer mediaPlayer) { 399 // Adjust the aspect ratio of this view 400 requestLayout(); 401 if (mSavedCurrentPosition > 0) { 402 mediaPlayer.seekTo(mSavedCurrentPosition); 403 mSavedCurrentPosition = 0; 404 } else { 405 // Start automatically 406 play(); 407 } 408 } 409 }); 410 mMediaPlayer.setOnCompletionListener( 411 new MediaPlayer.OnCompletionListener() { 412 @Override 413 public void onCompletion(MediaPlayer mediaPlayer) { 414 adjustToggleState(); 415 setKeepScreenOn(false); 416 if (mMovieListener != null) { 417 mMovieListener.onMovieStopped(); 418 } 419 } 420 }); 421 mMediaPlayer.prepare(); 422 } catch (IOException e) { 423 Log.e(TAG, "Failed to open video", e); 424 } 425 } 426 427 void closeVideo() { 428 if (mMediaPlayer != null) { 429 mMediaPlayer.release(); 430 mMediaPlayer = null; 431 } 432 } 433 434 void toggle() { 435 if (mMediaPlayer == null) { 436 return; 437 } 438 if (mMediaPlayer.isPlaying()) { 439 pause(); 440 } else { 441 play(); 442 } 443 } 444 445 void toggleControls() { 446 if (mShade.getVisibility() == View.VISIBLE) { 447 hideControls(); 448 } else { 449 showControls(); 450 } 451 } 452 453 void adjustToggleState() { 454 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 455 mToggle.setContentDescription(getResources().getString(R.string.pause)); 456 mToggle.setImageResource(R.drawable.ic_pause_64dp); 457 } else { 458 mToggle.setContentDescription(getResources().getString(R.string.play)); 459 mToggle.setImageResource(R.drawable.ic_play_arrow_64dp); 460 } 461 } 462 463 private static class TimeoutHandler extends Handler { 464 465 static final int MESSAGE_HIDE_CONTROLS = 1; 466 467 private final WeakReference<MovieView> mMovieViewRef; 468 469 TimeoutHandler(MovieView view) { 470 mMovieViewRef = new WeakReference<>(view); 471 } 472 473 @Override 474 public void handleMessage(Message msg) { 475 switch (msg.what) { 476 case MESSAGE_HIDE_CONTROLS: 477 MovieView movieView = mMovieViewRef.get(); 478 if (movieView != null) { 479 movieView.hideControls(); 480 } 481 break; 482 default: 483 super.handleMessage(msg); 484 } 485 } 486 } 487 } 488