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