Home | History | Annotate | Download | only in widget
      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